🔑 API Key Management

Generate and manage your API keys for accessing the ZwiftGopher API

Generate New API Key

A friendly name to give an idea what this key is for
View Documentation

API Documentation

Authentication

Include your API key in the Authorization header:

Authorization: Bearer sk_live_your_key_here

Rate Limiting

Limit: 1 request per 60 seconds per key / IP address

The API returns rate limit information in response headers:

X-RateLimit-Limit: 1
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 in riders)
  • Manual riders: "custom_riders": [{ "name": "Guest", "ftp": 250, "weight": 75, "height": 178 }]

Optional SI inputs:

  • "power_300_watts": 347 if you already know 5-minute power in watts
  • "power_300_wkg": 4.45 if 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.

Request JSON
{ "request_id": "team-a", "route": "next_wtrl", "team_name": "Saturday TTT", "riders": [354912, 6142432, 1909735] }
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.

Request JSON
{ "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 } } }
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.

Request JSON
{ "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 } ] }
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.

Request JSON
{ "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] } ] }
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.

Request JSON
{ "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 } ] } ] }
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.

Request JSON
{ "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 } ] } ] }
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

Current Build: 8 Riders Maximum
Default limit is 8 riders per optimization

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.
Overrides supported: name, ftp, weight, height, adjustment, power_300_watts, power_300_wkg.
Optional SI fields: 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]
        }
    ]
}