Your own walking pace (time model)
POST https://api.trailsplits.com/v1/time-model/fit
Checked by a live probe Key optional · 500 credits a day without one 5 credits per call
Send 1 to 30 of a person’s own timed walks. You get their pace against the TrailSplits hike time model on level ground, uphill and downhill, each with its uncertainty, as pace_profile. Pass it to route analysis, the route brief, route weather or go windows for that person’s own time beside the model’s. Nothing is kept: the walks are read, fitted and dropped, and the answer has no coordinates or times. A profile names its base_version; after a refit of the model it is refused (422 pace_profile_stale): fit again. 5 credits; at most 2 at a time per project.
Example
# Without a key, leave out the Authorization line (500 credits a day per address).
curl --fail-with-body 'https://api.trailsplits.com/v1/time-model/fit' \
-H "Authorization: Bearer $TRAILSPLITS_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"activities":[{"gpx":"<gpx>… your own timed walk …</gpx>"}]}' Request body
| Field | Type | Description |
|---|---|---|
activitiesrequired | array |
Response
| Field | Type | Description |
|---|---|---|
schema | string: time-model/1 | |
engine | object | |
base_model | string: trailsplits-hike/2 | |
base_version | string | |
pace_profile | object | |
by_class | object | |
activities | array | |
method | object | |
note | string | |
unknowns | array |
Full response schema (JSON Schema)
{
"type": "object",
"properties": {
"schema": {
"type": "string",
"enum": [
"time-model/1"
]
},
"engine": {
"type": "object"
},
"base_model": {
"type": "string",
"enum": [
"trailsplits-hike/2"
]
},
"base_version": {
"type": "string"
},
"pace_profile": {
"type": "object",
"properties": {
"base_model": {
"type": "string"
},
"base_version": {
"type": "string"
},
"factors": {
"type": "object",
"properties": {
"level": {
"type": "number"
},
"up": {
"type": "number"
},
"down": {
"type": "number"
}
},
"required": [
"level",
"up",
"down"
]
},
"log_sd": {
"type": "object",
"properties": {
"level": {
"type": "number"
},
"up": {
"type": "number"
},
"down": {
"type": "number"
}
},
"required": [
"level",
"up",
"down"
]
},
"day_log_sd": {
"type": "number"
},
"activities": {
"type": "integer"
}
},
"required": [
"base_version",
"factors",
"log_sd",
"day_log_sd"
]
},
"by_class": {
"type": "object"
},
"activities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"index": {
"type": "integer"
},
"used": {
"type": "boolean"
},
"reason": {
"type": "string",
"nullable": true
},
"length_m": {
"type": "number"
},
"moving_minutes": {
"type": "number"
},
"stopped_minutes": {
"type": "number"
},
"model_minutes": {
"type": "number"
}
},
"required": [
"index",
"used"
]
}
},
"method": {
"type": "object"
},
"note": {
"type": "string"
},
"unknowns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
},
"required": [
"schema",
"base_version",
"pace_profile",
"by_class",
"activities",
"unknowns"
]
}Errors
400, 413, 422, 503, plus the gateway's 401, 429 and 503 (every error code). A refused request costs no credits.
Contract
Send your recent timed walks. You get your own pace against the TrailSplits hike time model: how much faster or slower you are than the model on level ground, uphill and downhill, each with its uncertainty. Pass it back as pace_profile to route analysis, the route brief, route weather or go windows, and they give your own time beside the model's.
What it is, and what it is not.
- Nothing is kept. The activities are read, fitted and dropped. They are not stored, logged or kept in a receipt. The answer carries no coordinates and no times, only totals per activity and the fitted factors.
-
Relative to one model version. A profile names
base_version: the model and its constants (trailsplits-hike/2@0.96/0.18today). When TrailSplits refits the population model, every older profile is refused with 422pace_profile_stale. Fit again; old factors are never reused silently. - Moving time. Stops are left out: an interval where you neither cover 0.25 m/s nor climb or descend 0.05 m/s, and any interval over 5 minutes. Breaks are not in the profile, and not in the times it gives.
- Held back with little data. Each factor starts at the population model (1). One activity moves it only part of the way, and its uncertainty says how far. Five or more activities with real climbing and descending are better.
- The model's own sections. Your time is compared with the model section by section: about 100 m each, terrain heights smoothed ±25 m. Each section is level (under 5 % grade), up or down. Heights are the terrain model's at your points, the model's own basis, never your watch's.
Request
{ "activities": [
{ "gpx": "<gpx …><trk><trkseg><trkpt lat=\"45.82\" lon=\"6.97\"><time>2026-09-01T08:00:00Z</time></trkpt>…" },
{ "points": [[6.97, 45.82, "2026-09-03T07:30:00Z"], [6.971, 45.821, 1788420660], …] }
] }
| Field | |
|---|---|
activities | 1 to 30 recorded walks, each { gpx } (a GPX document with <time> on its track points) or { points } ([lon, lat, time] or [lon, lat, ele, time], time as ISO 8601 or epoch seconds; ele is ignored). At most 20,000 points each, 15 MB in all. Points without a time are left out; an activity with fewer than two timed points is refused |
caller | Optional label |
Response
200, time-model/1:
| Field | |
|---|---|
base_model, base_version | The population model the profile is relative to |
pace_profile | { base_model, base_version, factors: { level, up, down }, log_sd: { level, up, down }, day_log_sd, activities }. A factor of 1.2 means 20 % slower than the model. log_sd is each factor's uncertainty on a log scale; day_log_sd is how much your whole-walk time on a route the fit has not seen varies (day form and the model's own error on that route). Pass this object unchanged as pace_profile |
by_class | Per class: activities that counted, actual_minutes, model_minutes, factor, log_sd and interval_80 (the factor's 80 % interval) |
activities[] | Per activity, in your order: index, used, reason (why one was left out), length_m, moving_minutes, stopped_minutes, model_minutes, and by_class minutes. Nothing about where or when |
method, note | How the sections, moving time and fit are read |
unknowns[] | few_activities (under 3), no_level_ground, no_up_ground, no_down_ground (that factor stays 1) |
Using the profile:
-
Route analysis:
pace_profilewithprofile: hikeaddstime.personal { minutes, interval_80, base_version, factors, activities }.time.minutesstays the population model's. -
Route brief: the same in its analysis, and in
summaryaspersonal_minutesandpersonal_interval_80. The weather part is timed with it. -
Route weather and go windows:
pace_profiletimes the walk in place ofpace_factor(not both).pace.personalnames the factors.
Errors
Every refusal also carries docs (this page's code on /api/errors), and field, limit and got, hint, retry_after or request_id where they apply, as the error standard describes (api-errors-v1, since 6 Oct 2026).
JSON, { "error": { "code", "message" } }.
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request, invalid_gpx | Not JSON, an unknown field, a malformed activity or point |
| 400 | invalid_json | The body (or ?json=) is not valid JSON: send one JSON object with Content-Type: application/json |
| 405 | method_not_allowed | Any method but POST (and OPTIONS); the answer carries Allow: POST |
| 413 | body_too_large, too_many_points | Over 15 MB, or an activity over 20,000 points |
| 422 | activity_untimed | An activity with fewer than two timed points |
| 422 | no_usable_activity | No activity has 10 minutes of moving on ground the terrain model covers |
| 422 | pace_profile_stale | (In the consumers.) The profile was fitted against another base_version |
| 503 | overloaded, terrain_unavailable | As for route analysis; for the fit itself, overloaded means the API worker is behind and a request would wait more than about a second to start (Retry-After 1–5 s; since 5 Oct 2026) |
How it works
- Terrain heights are read at the activity's points: every point, or every k-th past 60,000 in all.
- Moving seconds per interval are spread over the model's sections by distance. Each activity then has actual and model minutes for level, up and down.
-
Per class: the posterior mean of log(actual / model). Each activity weighs in by its minutes in the class (full weight from 30 minutes, none under 5), with noise sd 0.12, against a normal prior of sd 0.3 around 0 (the population model). The factor is exp(mean), and
log_sdis the posterior sd. - A route's personal time is each class's factor times the model's minutes in that class. Its 80 % interval combines each factor's uncertainty, weighted by its share of the time, with the day-to-day spread.
Rough ground: the fit uses the calibration's median share of rough ground for every activity, because activities are not matched to the map. A walker who always hikes on rough ground therefore has slightly high factors, which route analysis then applies on top of its own rough-ground share. This is a known limit of v1.
Versioning
New fields may appear in time-model/1. A change of what a factor means is time-model/2. A refit of the base model changes base_version, not the schema.