Generate New API Key
API Documentation
Authentication
Include your API key in the Authorization header:
Rate Limiting
Limit: 1 request per 60 seconds per key / IP address
The API returns rate limit information in response headers:
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1234567890
Endpoints
POST /api/optimize
Run auto-optimize on a team composition and get the optimized plan.
Single teams and batch requests use the same route. Existing single-request clients do not need to change endpoint paths.
Request:
"request_id": "team-a",
"riders": [354912, 6142432, 1909735],
"team_name": "Saturday TTT",
"route": "next_wtrl",
"intensity": 0
}
Rider input formats:
- Array of IDs:
[354912, 6142432, 1909735] - ID-to-overrides map (separate):
"rider_overrides": { "354912": { "ftp": 239, "name": "Dave", "power_300_watts": 295 } }(IDs must exist inriders) - Manual riders:
"custom_riders": [{ "name": "Guest", "ftp": 250, "weight": 75, "height": 178 }]
Optional SI inputs:
"power_300_watts": 347if you already know 5-minute power in watts"power_300_wkg": 4.45if you already know 5-minute power in W/kg- Both fields can be sent directly inside rider objects or inside
rider_overrides - If omitted, SI falls back to
130% FTP
Example: direct rider objects with 300s power
"riders": [
{ "zwift_id": "354912", "power_300_watts": 295 },
{ "zwift_id": "751042", "power_300_wkg": 4.5 }
]
}
Example: rider_overrides with 300s power
"riders": [354912, 751042],
"rider_overrides": {
"354912": { "ftp": 239, "power_300_watts": 295 },
"751042": { "power_300_wkg": 4.5 }
}
}
Batch Mode
Use a top-level requests array when you want to optimize multiple teams in one API call. You can also use defaults for shared settings.
"defaults": {
"route": "next_wtrl",
"intensity": 1,
"duration_interval": 10
},
"requests": [
{
"request_id": "alpha",
"team_name": "Alpha",
"riders": [354912, 6142432, 1909735]
},
{
"request_id": "beta",
"team_name": "Beta",
"riders": [5652740, 579296, 2765354]
}
]
}
Batch limits: up to 20 optimize requests per batch. Each batch item succeeds or fails independently.
Response:
"success": true,
"data": {
"request_id": "team-a",
"route": "Canopies and Coastlines",
"estimated_time_seconds": 1947,
"estimated_avg_speed": 43.2,
"team_avg_power": 285,
"riders": [
{
"zwift_id": "354912",
"name": "Dave Edmonds",
"power_300_watts": 295,
"speed_index": 68,
"speed_index_source": "power_profile",
"...": "..."
}
]
},
"meta": { ... }
}
Batch response:
"success": true,
"data": {
"mode": "batch",
"results": [
{
"index": 0,
"request_id": "alpha",
"success": true,
"data": { ... }
},
{
"index": 1,
"request_id": "beta",
"success": false,
"error": "OPTIMIZE_ERROR",
"message": "At least 2 riders are required",
"status_code": 400
}
],
"summary": {
"total_requests": 2,
"successful_requests": 1,
"failed_requests": 1
}
}
}
Example Library
Choose a copy/paste starting point. These examples all use the same POST /api/optimize route.
Fastest single-team starting point. Send rider IDs and let the API fetch rider data.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"request_id": "team-a",
"route": "next_wtrl",
"team_name": "Saturday TTT",
"riders": [354912, 6142432, 1909735]
}'
Use IDs plus rider_overrides when you want to adjust FTP, name, body data, or 300-second power without sending a full rider object.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"request_id": "team-a",
"route": "next_wtrl",
"riders": [354912, 4421933, 6142432],
"rider_overrides": {
"354912": { "ftp": 239, "name": "Dave", "power_300_watts": 295 },
"4421933": { "weight": 90, "height": 185, "power_300_wkg": 4.2 }
}
}'
Use custom_riders when you want to optimize riders that are not looked up from ZwiftRacing/ZwiftPower.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"request_id": "manual-team",
"route": "next_wtrl",
"team_name": "Guest Squad",
"custom_riders": [
{ "name": "Guest Rider 1", "ftp": 260, "weight": 71, "height": 177, "power_300_watts": 335 },
{ "name": "Guest Rider 2", "ftp": 245, "weight": 66, "height": 170, "power_300_watts": 315 }
]
}'
Basic batch request for someone managing multiple teams. Shared settings live in defaults.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"route": "next_wtrl",
"intensity": 1,
"duration_interval": 10
},
"requests": [
{
"request_id": "alpha",
"team_name": "Alpha",
"riders": [354912, 751042, 4214146, 5339496]
},
{
"request_id": "beta",
"team_name": "Beta",
"riders": [5652740, 579296, 2765354, 987271]
}
]
}'
Mixed batch example. The first team uses IDs plus overrides. The second team uses manual riders. This is the best example when teams are managed differently.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"route": "next_wtrl",
"optimization_strategy": "variable",
"intensity": 0
},
"requests": [
{
"request_id": "wtrl-a",
"team_name": "WTRL A",
"riders": [354912, 751042, 4214146, 5339496],
"rider_overrides": {
"354912": { "ftp": 239, "name": "Dave", "power_300_watts": 295 },
"4214146": { "power_300_wkg": 4.6 }
}
},
{
"request_id": "guest-team",
"team_name": "Guest Team",
"custom_riders": [
{ "name": "Manual Rider 1", "ftp": 255, "weight": 69, "height": 175, "power_300_watts": 330 },
{ "name": "Manual Rider 2", "ftp": 242, "weight": 73, "height": 179, "power_300_watts": 312 },
{ "name": "Manual Rider 3", "ftp": 235, "weight": 64, "height": 168, "power_300_watts": 305 }
]
}
]
}'
Batch example with explicit SI hints. Useful when your own tooling already knows 300-second watts or W/kg and you want to avoid relying on fetched power profile data.
curl -X POST https://zwiftgopher.com/api/optimize \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"route": "next_wtrl",
"efficiency": 1
},
"requests": [
{
"request_id": "alpha-si",
"team_name": "Alpha SI",
"riders": [
{ "zwift_id": "354912", "power_300_watts": 295 },
{ "zwift_id": "751042", "power_300_wkg": 4.5 }
]
},
{
"request_id": "beta-si",
"team_name": "Beta SI",
"riders": [
{ "zwift_id": "5652740", "power_300_watts": 365 },
{ "zwift_id": "579296", "power_300_wkg": 4.7 }
]
}
]
}'
Notes:
Notes on rider data: Rider metadata (FTP, weight, height, name) is fetched from ZwiftRacing and ZwiftPower when not supplied explicitly. We recommend a script timeout (execution limit) of around 90 seconds to allow for data fetching and optimisation. Please post feedback, support and feature requests to the Discord server.
Optimization Settings Reference
The API supports various settings to customize the optimization behavior. All settings are optional and use sensible defaults.
Request Body Structure
"request_id": "team-a",
"riders": [ ... ],
"rider_overrides": { ... },
"custom_riders": [ ... ],
"route": "next",
"team_name": "API Team",
"target_speed": 40,
"intensity": 0,
"efficiency": 0,
"allow_zero_pulls": false,
"min_pull_duration": 30,
"max_pull_duration": 180,
"duration_interval": 15,
"optimization_strategy": "variable"
}
Batch Request Structure
"defaults": {
"route": "next_wtrl",
"intensity": 1
},
"requests": [
{
"request_id": "alpha",
"team_name": "Alpha",
"riders": [ ... ]
},
{
"request_id": "beta",
"team_name": "Beta",
"riders": [ ... ]
}
]
}
Rider Limits
Future Enhancement: A max_riders parameter will allow override up to 12 riders maximum (requires authentication upgrade)
Batch Mode: Up to 20 optimize requests can be included in a single batch call.
Available Settings
request_id
| Type | String |
| Default | Not set |
| Description | Optional client correlation ID echoed back in the response. Especially useful for batch mode. |
riders
| Type | Array (IDs only) |
| Required | No (but custom_riders must be populated instead) |
| Description | Provide Zwift rider IDs directly. Use rider_overrides for overrides. |
power_300_watts or power_300_wkg. If these are omitted, SI falls back to 130% FTP.
defaults
| Type | Object |
| Required | No |
| Description | Batch-only object for shared settings. Values here are applied to every item in requests, unless that item overrides them. |
requests
| Type | Array of optimize request objects |
| Required | No (used only for batch mode) |
| Max Items | 20 |
| Description | Switches the endpoint into batch mode. Each item uses the same request shape as a normal single optimize call. |
custom_riders
| Type | Array of objects |
| Required Fields | name, ftp, weight, height |
| Description | Manual riders without Zwift IDs. Each entry must include all four fields. |
team_name
| Type | String |
| Default | "API Team" |
| Description | Optional display name for the team in results. |
route
| Type | String |
| Default | "next" |
| Valid Values | "next", "next_wtrl", "next_zrl" |
| Description | Select which upcoming event schedule to use. wtrl refers to the Thursday TTT, zrl refers to the Zwift Racing League. |
intensity
| Type | Integer (-3 to +3) |
| Default | 0 (neutral) |
| Description | Adjusts the effort intensity of the optimization. Negative values reduce intensity/effort, positive values increase it. |
| Valid Range | -3 (easiest) to +3 (hardest) |
efficiency
| Type | Integer |
| Default | 0 (neutral) |
| Valid Range | -2 to +2 |
| Description | Scales the efficiency of power distribution. -2: average team efficiency (gaps, overlapping, messy rotations), +2: elite efficiency (perfect pace lines). |
allow_zero_pulls
| Type | Boolean |
| Default | false (disabled) |
| Description | When disabled, every rider must pull for at least the minimum duration. When enabled, riders can be designated as non-pullers (useful for recovering or weaker riders). |
min_pull_duration
| Type | Integer (seconds) |
| Default | 30 seconds |
| Valid Range | 10 to 120 seconds |
| Description | The minimum duration each pull must last. Shorter minimums allow more frequent rider changes. |
max_pull_duration
| Type | Integer (seconds) |
| Default | 180 seconds (3 minutes) |
| Valid Range | 30 to 600 seconds |
| Description | The maximum duration a single pull can last. Shorter maximums force more rotation. |
duration_interval
| Type | Integer (seconds) |
| Default | 15 seconds |
| Valid Range | 10 or 15 seconds only |
| Description | Time interval used for split calculations in the optimization algorithm. |
optimization_strategy
| Type | String |
| Default | "variable" |
| Valid Range | "variable" or "fixed" |
| Description |
"variable": Optimizer adjusts speed based on rider capability and power dynamics "fixed": Optimizes to a fixed pull speed (on the flat) for the whole team |
Example: Complete Single Request
{
"request_id": "team-a",
"riders": [354912, 4421933, 6142432],
"rider_overrides": {
"354912": { "ftp": 239, "name": "Dave", "power_300_watts": 295 },
"4421933": { "weight": 90, "height": 185, "power_300_wkg": 4.2 }
},
"team_name": "API Test Team",
"target_speed": 42,
"intensity": 1,
"efficiency": 1,
"allow_zero_pulls": false,
"min_pull_duration": 30,
"max_pull_duration": 180,
"duration_interval": 15,
"optimization_strategy": "variable"
}
Example: Complete Batch Request
{
"defaults": {
"route": "next_wtrl",
"intensity": 1,
"efficiency": 1,
"duration_interval": 15
},
"requests": [
{
"request_id": "alpha",
"team_name": "Alpha",
"riders": [354912, 751042, 4214146, 5339496]
},
{
"request_id": "beta",
"team_name": "Beta",
"riders": [5652740, 579296, 2765354, 987271]
}
]
}