{
 "openapi": "3.1.0",
 "info": {
  "title": "CorrConnect API",
  "version": "0.1.0",
  "description": "CorrConnect developer API. Early access: subject to change. Generated from the service's own OpenAPI document; operations for CorrConnect's own pages are left out."
 },
 "servers": [
  {
   "url": "{origin}",
   "variables": {
    "origin": {
     "default": "https://corrconnect.example",
     "description": "Your CorrConnect origin, sent with your invitation."
    }
   }
  }
 ],
 "tags": [
  {
   "name": "OAuth"
  },
  {
   "name": "Athlete data"
  },
  {
   "name": "Console session"
  },
  {
   "name": "Apps"
  },
  {
   "name": "Webhook subscriptions"
  },
  {
   "name": "Service"
  }
 ],
 "paths": {
  "/healthz": {
   "get": {
    "summary": "Check the service is up",
    "operationId": "health_healthz_get",
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "ok"
           ]
          }
         }
        },
        "example": {
         "status": "ok"
        }
       }
      }
     }
    },
    "tags": [
     "Service"
    ],
    "x-corrconnect-errors": [],
    "description": "Answers on both hosts without authentication.",
    "x-corrconnect-auth": "none",
    "security": []
   }
  },
  "/v1/account/register": {
   "post": {
    "summary": "Create an account",
    "operationId": "register_v1_account_register_post",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RegisterRequest"
       },
       "example": {
        "email": "you@developer.example",
        "password": "<at-least-12-characters>"
       }
      }
     },
     "required": true
    },
    "responses": {
     "201": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "type": "object",
           "required": [
            "verification_required",
            "message"
           ],
           "properties": {
            "verification_required": {
             "type": "boolean",
             "enum": [
              true
             ]
            },
            "message": {
             "type": "string"
            }
           }
          },
          {
           "type": "object",
           "required": [
            "user",
            "csrf_token",
            "expires_at"
           ],
           "properties": {
            "user": {
             "type": "object",
             "required": [
              "id",
              "email"
             ],
             "properties": {
              "id": {
               "type": "string",
               "example": "usr_0123456789abcdef0123456789abcdef"
              },
              "email": {
               "type": "string"
              }
             }
            },
            "csrf_token": {
             "type": "string"
            },
            "expires_at": {
             "type": "string",
             "format": "date-time"
            }
           }
          }
         ]
        },
        "example": {
         "verification_required": true,
         "message": "Check your email for a verification link."
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "tags": [
     "Console session"
    ],
    "x-corrconnect-errors": [
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 409,
      "code": "account_exists",
      "when": "An account already uses this email address."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "More than 5 sign-ups from one network address in an hour."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "description": "Creates a CorrConnect account and, where email verification is off, signs it in. Where verification is on (production), the response is {\"verification_required\": true, \"message\": ...} and the account signs in after the emailed link is opened.",
    "x-corrconnect-auth": "origin",
    "security": [],
    "parameters": [
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     }
    ]
   }
  },
  "/v1/account/login": {
   "post": {
    "summary": "Sign in",
    "operationId": "login_v1_account_login_post",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LoginRequest"
       },
       "example": {
        "email": "you@developer.example",
        "password": "<your-password>"
       }
      }
     },
     "required": true
    },
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "user",
          "csrf_token",
          "expires_at"
         ],
         "properties": {
          "user": {
           "type": "object",
           "required": [
            "id",
            "email"
           ],
           "properties": {
            "id": {
             "type": "string",
             "example": "usr_0123456789abcdef0123456789abcdef"
            },
            "email": {
             "type": "string"
            }
           }
          },
          "csrf_token": {
           "type": "string"
          },
          "expires_at": {
           "type": "string",
           "format": "date-time"
          }
         }
        },
        "example": {
         "user": {
          "id": "usr_0123456789abcdef0123456789abcdef",
          "email": "you@developer.example"
         },
         "csrf_token": "<csrf-token>",
         "expires_at": "2026-10-10T00:00:00+00:00"
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "tags": [
     "Console session"
    ],
    "x-corrconnect-errors": [
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 401,
      "code": "invalid_credentials",
      "when": "The email or password is wrong."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "More than 10 sign-in attempts from one network address in 15 minutes."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "description": "Sets the session cookie and returns a CSRF token for changes.",
    "x-corrconnect-auth": "origin",
    "security": [],
    "parameters": [
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     }
    ]
   }
  },
  "/v1/account/me": {
   "get": {
    "summary": "Get the session and a fresh CSRF token",
    "operationId": "me_v1_account_me_get",
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "user",
          "csrf_token"
         ],
         "properties": {
          "user": {
           "type": "object",
           "required": [
            "id",
            "email"
           ],
           "properties": {
            "id": {
             "type": "string",
             "example": "usr_0123456789abcdef0123456789abcdef"
            },
            "email": {
             "type": "string"
            }
           }
          },
          "csrf_token": {
           "type": "string"
          }
         }
        },
        "example": {
         "user": {
          "id": "usr_0123456789abcdef0123456789abcdef",
          "email": "you@developer.example"
         },
         "csrf_token": "<csrf-token>"
        }
       }
      }
     }
    },
    "tags": [
     "Console session"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     }
    ],
    "description": "Each call issues a new CSRF token and retires the previous one. Call it before a change.",
    "x-corrconnect-auth": "session",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/v1/account/logout": {
   "post": {
    "summary": "Sign out",
    "operationId": "logout_v1_account_logout_post",
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "signed_out"
           ]
          }
         }
        },
        "example": {
         "status": "signed_out"
        }
       }
      }
     }
    },
    "tags": [
     "Console session"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     }
    ],
    "description": "Ends this session and clears the cookie.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ],
    "parameters": [
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ]
   }
  },
  "/v1/developer/apps": {
   "get": {
    "summary": "List your apps",
    "operationId": "list_developer_apps_v1_developer_apps_get",
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "required": [
             "client_id",
             "name",
             "redirect_uris",
             "created_at",
             "rate_limits"
            ],
            "properties": {
             "client_id": {
              "type": "string",
              "example": "app_0123456789abcdef0123456789abcdef"
             },
             "name": {
              "type": "string"
             },
             "redirect_uris": {
              "type": "array",
              "items": {
               "type": "string"
              }
             },
             "created_at": {
              "type": "string",
              "format": "date-time"
             },
             "rate_limits": {
              "type": "object",
              "required": [
               "tier",
               "per_15_minutes",
               "per_day"
              ],
              "properties": {
               "tier": {
                "type": "string",
                "enum": [
                 "free",
                 "custom"
                ]
               },
               "per_15_minutes": {
                "type": "integer"
               },
               "per_day": {
                "type": "integer"
               }
              }
             }
            }
           }
          }
         }
        },
        "example": {
         "data": [
          {
           "client_id": "app_0123456789abcdef0123456789abcdef",
           "name": "Interval Notebook",
           "redirect_uris": [
            "https://developer.example/callback"
           ],
           "created_at": "2026-10-09T12:00:00+00:00",
           "rate_limits": {
            "tier": "free",
            "per_15_minutes": 100,
            "per_day": 10000
           }
          }
         ]
        }
       }
      }
     }
    },
    "tags": [
     "Apps"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     }
    ],
    "description": "Oldest first. rate_limits shows the free tier or a custom limit.",
    "x-corrconnect-auth": "session",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   },
   "post": {
    "summary": "Register an app",
    "operationId": "create_developer_app_v1_developer_apps_post",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/AppCreateRequest"
       },
       "example": {
        "name": "Interval Notebook",
        "redirect_uris": [
         "https://developer.example/callback"
        ]
       }
      }
     },
     "required": true
    },
    "responses": {
     "201": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "client_id",
          "name",
          "redirect_uris",
          "client_type",
          "rate_limits"
         ],
         "properties": {
          "client_id": {
           "type": "string"
          },
          "name": {
           "type": "string"
          },
          "redirect_uris": {
           "type": "array",
           "items": {
            "type": "string"
           }
          },
          "client_type": {
           "type": "string",
           "enum": [
            "public"
           ]
          },
          "rate_limits": {
           "type": "object",
           "required": [
            "tier",
            "per_15_minutes",
            "per_day"
           ],
           "properties": {
            "tier": {
             "type": "string",
             "enum": [
              "free",
              "custom"
             ]
            },
            "per_15_minutes": {
             "type": "integer"
            },
            "per_day": {
             "type": "integer"
            }
           }
          }
         }
        },
        "example": {
         "client_id": "app_0123456789abcdef0123456789abcdef",
         "name": "Interval Notebook",
         "redirect_uris": [
          "https://developer.example/callback"
         ],
         "rate_limits": {
          "tier": "free",
          "per_15_minutes": 100,
          "per_day": 10000
         },
         "client_type": "public"
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "tags": [
     "Apps"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 422,
      "code": "invalid_redirect_uri",
      "when": "A callback is not an exact HTTPS URL, or has a fragment, credentials or a wildcard."
     },
     {
      "status": 429,
      "code": "app_registration_rate_limited",
      "when": "More than 20 registrations from this account, or 60 from this network address, in an hour."
     },
     {
      "status": 409,
      "code": "app_limit_reached",
      "when": "The account already owns the maximum number of apps (10 by default)."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "description": "Registers a public client. There is no client secret. Keep client_id; it is not a credential.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ],
    "parameters": [
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ]
   }
  },
  "/v1/developer/apps/{app_id}": {
   "delete": {
    "summary": "Delete an app",
    "operationId": "revoke_developer_app_v1_developer_apps__app_id__delete",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ],
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "revoked"
           ]
          }
         }
        },
        "example": {
         "status": "revoked"
        }
       }
      }
     }
    },
    "tags": [
     "Apps"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     }
    ],
    "description": "Deletes the app with its grants, codes, tokens and webhook subscriptions. Athletes' grants end at once.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/oauth/authorize": {
   "get": {
    "operationId": "authorization_page_oauth_authorize_get",
    "parameters": [
     {
      "name": "response_type",
      "in": "query",
      "required": true,
      "description": "Always code.",
      "schema": {
       "type": "string",
       "enum": [
        "code"
       ]
      }
     },
     {
      "name": "client_id",
      "in": "query",
      "required": true,
      "description": "Your app's client_id.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "redirect_uri",
      "in": "query",
      "required": true,
      "description": "One of the app's registered callbacks, character for character.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "scope",
      "in": "query",
      "required": true,
      "description": "Space-separated scopes, for example activities:read profile:read.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "state",
      "in": "query",
      "required": true,
      "description": "Random value of at least 8 characters; it comes back unchanged.",
      "schema": {
       "type": "string",
       "minLength": 8
      }
     },
     {
      "name": "code_challenge",
      "in": "query",
      "required": true,
      "description": "BASE64URL(SHA256(code_verifier)) without padding (43 to 128 characters).",
      "schema": {
       "type": "string",
       "minLength": 43,
       "maxLength": 128
      }
     },
     {
      "name": "code_challenge_method",
      "in": "query",
      "required": true,
      "description": "Always S256.",
      "schema": {
       "type": "string",
       "enum": [
        "S256"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The HTML sign-in and consent page. After the athlete decides, the page sends the browser to redirect_uri with code and state, or error=access_denied and state."
     }
    },
    "security": [],
    "tags": [
     "OAuth"
    ],
    "x-corrconnect-errors": [
     {
      "status": 400,
      "code": "unsupported_authorization_request",
      "when": "response_type is not code or code_challenge_method is not S256."
     },
     {
      "status": 400,
      "code": "invalid_authorization_request",
      "when": "state is shorter than 8 characters or code_challenge is malformed."
     },
     {
      "status": 400,
      "code": "invalid_client",
      "when": "No app has this client_id."
     },
     {
      "status": 400,
      "code": "redirect_uri_mismatch",
      "when": "redirect_uri is not one of the app's registered callbacks, exactly."
     },
     {
      "status": 400,
      "code": "invalid_scope",
      "when": "A requested scope is unknown or the scope list is empty."
     }
    ],
    "summary": "Ask an athlete for access",
    "description": "Browser page, not an API call. Send the athlete here; CorrConnect checks every value before it shows sign-in. After the athlete decides, the browser goes to redirect_uri with code and state (approved) or error=access_denied and state (denied). A request that fails the checks gets a 400 error page and is not redirected.",
    "x-corrconnect-auth": "none"
   }
  },
  "/oauth/token": {
   "post": {
    "summary": "Exchange a code or refresh a token",
    "operationId": "token_exchange_oauth_token_post",
    "requestBody": {
     "content": {
      "application/x-www-form-urlencoded": {
       "schema": {
        "$ref": "#/components/schemas/Body_token_exchange_oauth_token_post"
       },
       "example": {
        "grant_type": "authorization_code",
        "client_id": "app_0123456789abcdef0123456789abcdef",
        "code": "<one-use-code>",
        "redirect_uri": "https://developer.example/callback",
        "code_verifier": "<43-to-128-character-PKCE-verifier>"
       }
      }
     },
     "required": true
    },
    "responses": {
     "200": {
      "description": "One-hour default bearer token; actual lifetime is expires_in seconds.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
         ],
         "properties": {
          "access_token": {
           "type": "string"
          },
          "token_type": {
           "type": "string",
           "enum": [
            "Bearer"
           ]
          },
          "expires_in": {
           "type": "integer"
          },
          "scope": {
           "type": "string"
          },
          "refresh_token": {
           "type": "string"
          },
          "refresh_token_expires_in": {
           "type": "integer"
          }
         }
        },
        "example": {
         "access_token": "<opaque-access-token>",
         "token_type": "Bearer",
         "expires_in": 3600,
         "scope": "activities:read profile:read",
         "refresh_token": "<opaque-refresh-token>",
         "refresh_token_expires_in": 5184000
        }
       }
      }
     },
     "400": {
      "description": "invalid_client, invalid_request, invalid_grant or unsupported_grant_type.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "error",
          "error_description",
          "request_id"
         ],
         "properties": {
          "error": {
           "type": "string"
          },
          "error_description": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          }
         }
        },
        "example": {
         "error": "invalid_grant",
         "error_description": "The authorization code is invalid, expired, used, or revoked.",
         "request_id": "req_example"
        }
       }
      }
     },
     "429": {
      "description": "Token exchange limit reached.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "error",
          "error_description",
          "request_id"
         ],
         "properties": {
          "error": {
           "type": "string"
          },
          "error_description": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          }
         }
        },
        "example": {
         "error": "slow_down",
         "error_description": "Request limit reached; retry later.",
         "request_id": "req_example"
        }
       }
      },
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       }
      }
     }
    },
    "security": [],
    "description": "Public-client token endpoint; no client secret. grant_type=authorization_code exchanges a one-use code with its PKCE verifier. grant_type=refresh_token sends client_id and refresh_token (and optionally a narrower scope); each refresh token works once and the response carries its replacement. Presenting a used refresh token again revokes the whole grant.",
    "tags": [
     "OAuth"
    ],
    "x-corrconnect-errors": [
     {
      "status": 400,
      "code": "invalid_request",
      "when": "A required field is missing or malformed."
     },
     {
      "status": 400,
      "code": "invalid_client",
      "when": "client_id is not a CorrConnect app ID."
     },
     {
      "status": 400,
      "code": "invalid_grant",
      "when": "The code or refresh token is invalid, expired, used or revoked, the verifier or redirect_uri does not match, or a refresh asked for scopes the grant lacks."
     },
     {
      "status": 400,
      "code": "unsupported_grant_type",
      "when": "grant_type is not authorization_code or refresh_token."
     },
     {
      "status": 429,
      "code": "slow_down",
      "when": "Too many failed attempts, or too many token requests from one address."
     }
    ],
    "x-corrconnect-auth": "none"
   }
  },
  "/oauth/revoke": {
   "post": {
    "summary": "End the whole grant",
    "operationId": "revoke_client_grant_oauth_revoke_post",
    "responses": {
     "200": {
      "description": "Idempotent release; token existence is not disclosed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "revoked"
           ]
          }
         }
        },
        "example": {
         "status": "revoked"
        }
       }
      }
     },
     "401": {
      "description": "Bearer header is missing or malformed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_access_token",
         "message": "Supply a valid Bearer access token.",
         "request_id": "req_example"
        }
       }
      }
     }
    },
    "security": [
     {
      "CorrConnectBearer": []
     }
    ],
    "tags": [
     "OAuth"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "invalid_access_token",
      "when": "No Bearer header was sent."
     }
    ],
    "description": "Send the access token as a Bearer header. The athlete's grant for your app ends with every code and token in it, and subscribed apps get grant.revoked. Expired tokens still work here; unknown tokens get the same answer.",
    "x-corrconnect-auth": "bearer"
   }
  },
  "/oauth/revoke-token": {
   "post": {
    "summary": "Revoke one token pair",
    "description": "Releases one token and the token issued with it; the grant stays active. Send the access token as a Bearer header, or a form body with token (access or refresh), optionally token_type_hint and client_id, as in RFC 7009.",
    "operationId": "revoke_client_token_oauth_revoke_token_post",
    "requestBody": {
     "content": {
      "application/x-www-form-urlencoded": {
       "schema": {
        "$ref": "#/components/schemas/Body_revoke_client_token_oauth_revoke_token_post"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Idempotent release; token existence is not disclosed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "revoked"
           ]
          }
         }
        },
        "example": {
         "status": "revoked"
        }
       }
      }
     },
     "401": {
      "description": "Bearer header is missing or malformed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_access_token",
         "message": "Supply a valid Bearer access token.",
         "request_id": "req_example"
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "CorrConnectBearer": []
     }
    ],
    "tags": [
     "OAuth"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "invalid_access_token",
      "when": "Neither a Bearer header nor a token field was sent."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "x-corrconnect-auth": "bearer"
   }
  },
  "/v1/activities": {
   "get": {
    "summary": "List activities",
    "operationId": "developer_activities_v1_activities_get",
    "parameters": [
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 25
      },
      "description": "How many records to return."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "anyOf": [
        {
         "type": "string"
        },
        {
         "type": "null"
        }
       ],
       "title": "Cursor"
      },
      "description": "The next_cursor from the previous page. Never build or edit one."
     },
     {
      "name": "source",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "manual_json",
        "gpx",
        "tcx",
        "fit"
       ]
      },
      "description": "Only records from this source."
     }
    ],
    "responses": {
     "200": {
      "description": "Normalized activity page.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "next_cursor"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "description": "Normalized summary. Optional provenance and file structure vary by source.",
            "required": [
             "id",
             "source",
             "activity_type",
             "started_at",
             "duration_seconds",
             "created_at"
            ],
            "properties": {
             "id": {
              "type": "string",
              "example": "act_0123456789abcdef0123456789abcdef",
              "description": "CorrConnect activity ID (act_ and 32 hex characters)."
             },
             "source": {
              "type": "string",
              "enum": [
               "manual_json",
               "gpx",
               "tcx",
               "fit"
              ],
              "description": "Where the record came from. Treat values you do not know as other sources."
             },
             "activity_type": {
              "type": "string",
              "enum": [
               "run",
               "ride",
               "swim",
               "walk",
               "strength",
               "other"
              ],
              "description": "Normalized sport."
             },
             "started_at": {
              "type": "string",
              "format": "date-time",
              "description": "Start time with its UTC offset."
             },
             "duration_seconds": {
              "type": "number",
              "description": "Duration in seconds; duration_basis says how it was measured when the source tells us."
             },
             "distance_meters": {
              "anyOf": [
               {
                "type": "number"
               },
               {
                "type": "null"
               }
              ],
              "description": "Distance in meters, or absent or null when the source has none."
             },
             "title": {
              "anyOf": [
               {
                "type": "string"
               },
               {
                "type": "null"
               }
              ],
              "description": "Name the athlete or file gave it."
             },
             "duration_basis": {
              "type": "string",
              "description": "How duration_seconds was measured, for example recorded_lap_sum."
             },
             "distance_basis": {
              "type": "string",
              "description": "How distance_meters was measured."
             },
             "provenance": {
              "type": "object",
              "description": "File format and position, for example {\"format\": \"FIT\", \"session_index\": 1}."
             },
             "segments": {
              "type": "array",
              "items": {
               "type": "object"
              },
              "description": "GPX segments: point counts, start and end times, elapsed seconds."
             },
             "laps": {
              "type": "array",
              "items": {
               "type": "object"
              },
              "description": "TCX or FIT laps: start, duration and distance. Never raw GPS points."
             },
             "created_at": {
              "type": "string",
              "format": "date-time",
              "description": "When CorrConnect stored it. Lists sort on this, newest first."
             }
            },
            "additionalProperties": true
           }
          },
          "next_cursor": {
           "anyOf": [
            {
             "type": "string"
            },
            {
             "type": "null"
            }
           ]
          }
         }
        },
        "example": {
         "data": [
          {
           "id": "act_0123456789abcdef0123456789abcdef",
           "source": "manual_json",
           "activity_type": "run",
           "started_at": "2026-09-22T11:15:00+00:00",
           "duration_seconds": 2400,
           "distance_meters": 8046.7,
           "created_at": "2026-09-22T11:16:00+00:00"
          }
         ],
         "next_cursor": null
        }
       }
      },
      "headers": {
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     },
     "400": {
      "description": "Invalid source, limit, or cursor.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_cursor",
         "message": "Use the next_cursor returned by CorrConnect.",
         "request_id": "req_example"
        }
       }
      }
     },
     "401": {
      "description": "Bearer token is missing, expired, or revoked.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_access_token",
         "message": "This access token is expired or revoked.",
         "request_id": "req_example"
        }
       }
      }
     },
     "403": {
      "description": "Bearer token lacks the required scope.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "insufficient_scope",
         "message": "The required scope is missing.",
         "request_id": "req_example"
        }
       }
      }
     },
     "429": {
      "description": "rate_limit_exceeded: the app (100 per 15 minutes, 10,000 per day) or one athlete (30 per 15 minutes, 2,000 per day) reached its limit; details.scope says which. service_capacity_exceeded: the shared ceiling for all third-party apps.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "rate_limit_exceeded",
         "message": "This app has used its request limit; retry after 60 seconds.",
         "request_id": "req_example",
         "details": {
          "per_15_minutes": 100,
          "per_day": 10000
         }
        }
       }
      },
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "CorrConnectBearer": []
     }
    ],
    "description": "The athlete's activities, newest first by when CorrConnect stored them. Pass next_cursor back as cursor for the next page; null means you have them all.",
    "tags": [
     "Athlete data"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "invalid_access_token",
      "when": "The Bearer token is missing, malformed, expired or revoked."
     },
     {
      "status": 403,
      "code": "insufficient_scope",
      "when": "The token does not carry the scope this endpoint needs."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "Your app, or this athlete within your app, used its request limit."
     },
     {
      "status": 429,
      "code": "service_capacity_exceeded",
      "when": "All third-party apps together reached the shared ceiling."
     },
     {
      "status": 400,
      "code": "invalid_source",
      "when": "source is not one of the listed values."
     },
     {
      "status": 400,
      "code": "invalid_limit",
      "when": "limit is outside 1 to 100."
     },
     {
      "status": 400,
      "code": "invalid_cursor",
      "when": "cursor is not a next_cursor value CorrConnect returned."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "x-corrconnect-auth": "bearer"
   }
  },
  "/v1/profile": {
   "get": {
    "summary": "Get the athlete's subject",
    "operationId": "developer_profile_v1_profile_get",
    "responses": {
     "200": {
      "description": "Pairwise profile subject.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "sub"
         ],
         "properties": {
          "sub": {
           "type": "string",
           "description": "ccu_ and 32 hex characters: this athlete's ID for your app."
          }
         }
        },
        "example": {
         "sub": "ccu_0123456789abcdef0123456789abcdef"
        }
       }
      },
      "headers": {
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     },
     "401": {
      "description": "Bearer token is missing, expired, or revoked.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_access_token",
         "message": "This access token is expired or revoked.",
         "request_id": "req_example"
        }
       }
      }
     },
     "403": {
      "description": "Bearer token lacks the required scope.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "insufficient_scope",
         "message": "The required scope is missing.",
         "request_id": "req_example"
        }
       }
      }
     },
     "429": {
      "description": "rate_limit_exceeded: the app (100 per 15 minutes, 10,000 per day) or one athlete (30 per 15 minutes, 2,000 per day) reached its limit; details.scope says which. service_capacity_exceeded: the shared ceiling for all third-party apps.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "rate_limit_exceeded",
         "message": "This app has used its request limit; retry after 60 seconds.",
         "request_id": "req_example",
         "details": {
          "per_15_minutes": 100,
          "per_day": 10000
         }
        }
       }
      },
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     }
    },
    "security": [
     {
      "CorrConnectBearer": []
     }
    ],
    "description": "Returns sub, this athlete's ID for your app. Every app sees a different sub for the same athlete, and it stays the same if they revoke and approve again. No email or name.",
    "tags": [
     "Athlete data"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "invalid_access_token",
      "when": "The Bearer token is missing, malformed, expired or revoked."
     },
     {
      "status": 403,
      "code": "insufficient_scope",
      "when": "The token does not carry the scope this endpoint needs."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "Your app, or this athlete within your app, used its request limit."
     },
     {
      "status": 429,
      "code": "service_capacity_exceeded",
      "when": "All third-party apps together reached the shared ceiling."
     }
    ],
    "x-corrconnect-auth": "bearer"
   }
  },
  "/v1/activities/{activity_id}": {
   "get": {
    "summary": "Get one activity",
    "operationId": "developer_activity_v1_activities__activity_id__get",
    "parameters": [
     {
      "name": "activity_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "Activity Id"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Normalized activity summary.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "description": "Normalized summary. Optional provenance and file structure vary by source.",
         "required": [
          "id",
          "source",
          "activity_type",
          "started_at",
          "duration_seconds",
          "created_at"
         ],
         "properties": {
          "id": {
           "type": "string",
           "example": "act_0123456789abcdef0123456789abcdef",
           "description": "CorrConnect activity ID (act_ and 32 hex characters)."
          },
          "source": {
           "type": "string",
           "enum": [
            "manual_json",
            "gpx",
            "tcx",
            "fit"
           ],
           "description": "Where the record came from. Treat values you do not know as other sources."
          },
          "activity_type": {
           "type": "string",
           "enum": [
            "run",
            "ride",
            "swim",
            "walk",
            "strength",
            "other"
           ],
           "description": "Normalized sport."
          },
          "started_at": {
           "type": "string",
           "format": "date-time",
           "description": "Start time with its UTC offset."
          },
          "duration_seconds": {
           "type": "number",
           "description": "Duration in seconds; duration_basis says how it was measured when the source tells us."
          },
          "distance_meters": {
           "anyOf": [
            {
             "type": "number"
            },
            {
             "type": "null"
            }
           ],
           "description": "Distance in meters, or absent or null when the source has none."
          },
          "title": {
           "anyOf": [
            {
             "type": "string"
            },
            {
             "type": "null"
            }
           ],
           "description": "Name the athlete or file gave it."
          },
          "duration_basis": {
           "type": "string",
           "description": "How duration_seconds was measured, for example recorded_lap_sum."
          },
          "distance_basis": {
           "type": "string",
           "description": "How distance_meters was measured."
          },
          "provenance": {
           "type": "object",
           "description": "File format and position, for example {\"format\": \"FIT\", \"session_index\": 1}."
          },
          "segments": {
           "type": "array",
           "items": {
            "type": "object"
           },
           "description": "GPX segments: point counts, start and end times, elapsed seconds."
          },
          "laps": {
           "type": "array",
           "items": {
            "type": "object"
           },
           "description": "TCX or FIT laps: start, duration and distance. Never raw GPS points."
          },
          "created_at": {
           "type": "string",
           "format": "date-time",
           "description": "When CorrConnect stored it. Lists sort on this, newest first."
          }
         },
         "additionalProperties": true
        },
        "example": {
         "id": "act_0123456789abcdef0123456789abcdef",
         "source": "fit",
         "activity_type": "ride",
         "started_at": "2026-09-22T11:15:00+00:00",
         "duration_seconds": 3600,
         "created_at": "2026-09-22T11:16:00+00:00"
        }
       }
      },
      "headers": {
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     },
     "404": {
      "description": "Unknown ID or another athlete's activity.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "activity_not_found",
         "message": "The activity was not found.",
         "request_id": "req_example"
        }
       }
      }
     },
     "401": {
      "description": "Bearer token is missing, expired, or revoked.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "invalid_access_token",
         "message": "This access token is expired or revoked.",
         "request_id": "req_example"
        }
       }
      }
     },
     "403": {
      "description": "Bearer token lacks the required scope.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "insufficient_scope",
         "message": "The required scope is missing.",
         "request_id": "req_example"
        }
       }
      }
     },
     "429": {
      "description": "rate_limit_exceeded: the app (100 per 15 minutes, 10,000 per day) or one athlete (30 per 15 minutes, 2,000 per day) reached its limit; details.scope says which. service_capacity_exceeded: the shared ceiling for all third-party apps.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "rate_limit_exceeded",
         "message": "This app has used its request limit; retry after 60 seconds.",
         "request_id": "req_example",
         "details": {
          "per_15_minutes": 100,
          "per_day": 10000
         }
        }
       }
      },
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "Limit of the most constrained window for this app."
       },
       "X-RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       },
       "RateLimit-Policy": {
        "schema": {
         "type": "string"
        },
        "example": "100;w=900, 10000;w=86400"
       },
       "RateLimit-Limit": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "RateLimit-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Seconds until that window resets."
       },
       "X-RateLimit-User-Limit": {
        "schema": {
         "type": "integer"
        },
        "description": "This athlete's sub-limit in the app (free tier: 30 per 15 minutes)."
       },
       "X-RateLimit-User-Remaining": {
        "schema": {
         "type": "integer"
        }
       },
       "X-RateLimit-User-Reset": {
        "schema": {
         "type": "integer"
        },
        "description": "Unix time when that window resets."
       }
      }
     }
    },
    "security": [
     {
      "CorrConnectBearer": []
     }
    ],
    "description": "One activity belonging to the token's athlete, in the same shape as list items. Use it with the activity_id from an activity.created event.",
    "tags": [
     "Athlete data"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "invalid_access_token",
      "when": "The Bearer token is missing, malformed, expired or revoked."
     },
     {
      "status": 403,
      "code": "insufficient_scope",
      "when": "The token does not carry the scope this endpoint needs."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "Your app, or this athlete within your app, used its request limit."
     },
     {
      "status": 429,
      "code": "service_capacity_exceeded",
      "when": "All third-party apps together reached the shared ceiling."
     },
     {
      "status": 404,
      "code": "activity_not_found",
      "when": "No activity with this ID belongs to the token's athlete."
     }
    ],
    "x-corrconnect-auth": "bearer"
   }
  },
  "/v1/developer/apps/{app_id}/webhooks": {
   "post": {
    "summary": "Create a webhook subscription",
    "operationId": "create_webhook_v1_developer_apps__app_id__webhooks_post",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WebhookCreateRequest"
       },
       "example": {
        "callback_url": "https://developer.example/hooks/corrconnect",
        "events": [
         "activity.created",
         "grant.revoked"
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "id",
          "callback_url",
          "events",
          "status",
          "consecutive_failures",
          "created_at",
          "updated_at",
          "secret"
         ],
         "properties": {
          "id": {
           "type": "string",
           "example": "whs_0123456789abcdef0123456789abcdef"
          },
          "callback_url": {
           "type": "string"
          },
          "events": {
           "type": "array",
           "items": {
            "type": "string",
            "enum": [
             "activity.created",
             "grant.revoked"
            ]
           }
          },
          "status": {
           "type": "string",
           "enum": [
            "active",
            "disabled"
           ]
          },
          "consecutive_failures": {
           "type": "integer"
          },
          "created_at": {
           "type": "string",
           "format": "date-time"
          },
          "updated_at": {
           "type": "string",
           "format": "date-time"
          },
          "secret": {
           "type": "string"
          }
         }
        },
        "example": {
         "id": "whs_0123456789abcdef0123456789abcdef",
         "callback_url": "https://developer.example/hooks/corrconnect",
         "events": [
          "activity.created",
          "grant.revoked"
         ],
         "status": "active",
         "consecutive_failures": 0,
         "created_at": "2026-10-09T12:00:00+00:00",
         "updated_at": "2026-10-09T12:00:00+00:00",
         "secret": "whsec_<shown-once>"
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "More than 20 subscriptions created by this account in an hour."
     },
     {
      "status": 400,
      "code": "invalid_callback_url",
      "when": "The URL is not public HTTPS on port 443, or has credentials, a fragment or a wildcard."
     },
     {
      "status": 400,
      "code": "invalid_event_type",
      "when": "events lists something other than activity.created or grant.revoked."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     },
     {
      "status": 409,
      "code": "webhook_limit_reached",
      "when": "The app already has 3 subscriptions."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "description": "The response carries secret (whsec_...) this once. Store it; CorrConnect never shows it again. events is optional and defaults to activity.created and grant.revoked.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   },
   "get": {
    "summary": "List webhook subscriptions",
    "operationId": "list_webhooks_v1_developer_apps__app_id__webhooks_get",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "required": [
             "id",
             "callback_url",
             "events",
             "status",
             "consecutive_failures",
             "created_at",
             "updated_at"
            ],
            "properties": {
             "id": {
              "type": "string",
              "example": "whs_0123456789abcdef0123456789abcdef"
             },
             "callback_url": {
              "type": "string"
             },
             "events": {
              "type": "array",
              "items": {
               "type": "string",
               "enum": [
                "activity.created",
                "grant.revoked"
               ]
              }
             },
             "status": {
              "type": "string",
              "enum": [
               "active",
               "disabled"
              ]
             },
             "consecutive_failures": {
              "type": "integer"
             },
             "created_at": {
              "type": "string",
              "format": "date-time"
             },
             "updated_at": {
              "type": "string",
              "format": "date-time"
             }
            }
           }
          }
         }
        },
        "example": {
         "data": [
          {
           "id": "whs_0123456789abcdef0123456789abcdef",
           "callback_url": "https://developer.example/hooks/corrconnect",
           "events": [
            "activity.created",
            "grant.revoked"
           ],
           "status": "active",
           "consecutive_failures": 0,
           "created_at": "2026-10-09T12:00:00+00:00",
           "updated_at": "2026-10-09T12:00:00+00:00"
          }
         ]
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     }
    ],
    "description": "Oldest first. Secrets are never returned.",
    "x-corrconnect-auth": "session",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/v1/developer/apps/{app_id}/webhooks/{subscription_id}": {
   "delete": {
    "summary": "Delete a webhook subscription",
    "operationId": "delete_webhook_v1_developer_apps__app_id__webhooks__subscription_id__delete",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "subscription_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "Subscription Id"
      }
     },
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ],
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "deleted"
           ]
          }
         }
        },
        "example": {
         "status": "deleted"
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     },
     {
      "status": 404,
      "code": "webhook_not_found",
      "when": "The app has no subscription with this ID."
     }
    ],
    "description": "Removes the subscription and its delivery log.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/v1/developer/apps/{app_id}/webhooks/{subscription_id}/enable": {
   "post": {
    "summary": "Turn a paused subscription back on",
    "operationId": "enable_webhook_v1_developer_apps__app_id__webhooks__subscription_id__enable_post",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "subscription_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "Subscription Id"
      }
     },
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ],
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "id",
          "callback_url",
          "events",
          "status",
          "consecutive_failures",
          "created_at",
          "updated_at"
         ],
         "properties": {
          "id": {
           "type": "string",
           "example": "whs_0123456789abcdef0123456789abcdef"
          },
          "callback_url": {
           "type": "string"
          },
          "events": {
           "type": "array",
           "items": {
            "type": "string",
            "enum": [
             "activity.created",
             "grant.revoked"
            ]
           }
          },
          "status": {
           "type": "string",
           "enum": [
            "active",
            "disabled"
           ]
          },
          "consecutive_failures": {
           "type": "integer"
          },
          "created_at": {
           "type": "string",
           "format": "date-time"
          },
          "updated_at": {
           "type": "string",
           "format": "date-time"
          }
         }
        },
        "example": {
         "id": "whs_0123456789abcdef0123456789abcdef",
         "callback_url": "https://developer.example/hooks/corrconnect",
         "events": [
          "activity.created",
          "grant.revoked"
         ],
         "status": "active",
         "consecutive_failures": 0,
         "created_at": "2026-10-09T12:00:00+00:00",
         "updated_at": "2026-10-09T12:00:00+00:00"
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "More than 20 enables by this account in an hour."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     },
     {
      "status": 404,
      "code": "webhook_not_found",
      "when": "The app has no subscription with this ID."
     }
    ],
    "description": "A subscription pauses after 20 failed attempts in a row. This sets it active and resets the count.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/v1/developer/apps/{app_id}/webhooks/{subscription_id}/test": {
   "post": {
    "summary": "Send a test event",
    "operationId": "test_webhook_v1_developer_apps__app_id__webhooks__subscription_id__test_post",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "subscription_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "Subscription Id"
      }
     },
     {
      "name": "Origin",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Exactly the CorrConnect portal origin."
     },
     {
      "name": "X-CSRF-Token",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The latest csrf_token from sign-in or GET /v1/account/me."
     }
    ],
    "responses": {
     "202": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "status",
          "delivery_id"
         ],
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "queued"
           ]
          },
          "delivery_id": {
           "type": "string"
          }
         }
        },
        "example": {
         "status": "queued",
         "delivery_id": "whd_0123456789abcdef0123456789abcdef"
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 403,
      "code": "origin_rejected",
      "when": "The Origin header is not the exact CorrConnect portal origin."
     },
     {
      "status": 403,
      "code": "csrf_rejected",
      "when": "X-CSRF-Token is missing or is not the latest token from GET /v1/account/me."
     },
     {
      "status": 429,
      "code": "rate_limit_exceeded",
      "when": "More than 30 test events from this account in an hour."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     },
     {
      "status": 404,
      "code": "webhook_not_found",
      "when": "The app has no subscription with this ID."
     },
     {
      "status": 409,
      "code": "webhook_disabled",
      "when": "The subscription is paused; enable it first."
     }
    ],
    "description": "Queues one webhook.test event to this subscription, signed like any other event.",
    "x-corrconnect-auth": "session_write",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  },
  "/v1/developer/apps/{app_id}/webhooks/{subscription_id}/deliveries": {
   "get": {
    "summary": "List recent deliveries",
    "operationId": "list_webhook_deliveries_v1_developer_apps__app_id__webhooks__subscription_id__deliveries_get",
    "parameters": [
     {
      "name": "app_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "App Id"
      }
     },
     {
      "name": "subscription_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "title": "Subscription Id"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "maximum": 100,
       "minimum": 1,
       "default": 50,
       "title": "Limit"
      },
      "description": "How many deliveries to return, newest first."
     }
    ],
    "responses": {
     "200": {
      "description": "Successful Response",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "required": [
             "id",
             "event_id",
             "event_type",
             "status",
             "attempts",
             "created_at",
             "updated_at"
            ],
            "properties": {
             "id": {
              "type": "string",
              "example": "whd_0123456789abcdef0123456789abcdef"
             },
             "event_id": {
              "type": "string"
             },
             "event_type": {
              "type": "string",
              "enum": [
               "activity.created",
               "grant.revoked",
               "webhook.test"
              ]
             },
             "status": {
              "type": "string",
              "enum": [
               "pending",
               "delivered",
               "failed"
              ]
             },
             "attempts": {
              "type": "integer"
             },
             "last_status_code": {
              "anyOf": [
               {
                "type": "integer"
               },
               {
                "type": "null"
               }
              ]
             },
             "last_error": {
              "anyOf": [
               {
                "type": "string",
                "enum": [
                 "http_status",
                 "delivery_failed",
                 "invalid_destination",
                 "subscription_disabled"
                ]
               },
               {
                "type": "null"
               }
              ]
             },
             "created_at": {
              "type": "string",
              "format": "date-time"
             },
             "updated_at": {
              "type": "string",
              "format": "date-time"
             }
            }
           }
          }
         }
        },
        "example": {
         "data": [
          {
           "id": "whd_0123456789abcdef0123456789abcdef",
           "event_id": "evt_0123456789abcdef0123456789abcdef",
           "event_type": "activity.created",
           "status": "delivered",
           "attempts": 1,
           "last_status_code": 200,
           "last_error": null,
           "created_at": "2026-10-09T12:00:00+00:00",
           "updated_at": "2026-10-09T12:00:01+00:00"
          }
         ]
        }
       }
      }
     },
     "422": {
      "description": "validation_failed with details.fields.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "code",
          "message",
          "request_id"
         ],
         "properties": {
          "code": {
           "type": "string"
          },
          "message": {
           "type": "string"
          },
          "request_id": {
           "type": "string"
          },
          "details": {
           "type": "object"
          }
         }
        },
        "example": {
         "code": "validation_failed",
         "message": "Review the invalid request fields.",
         "request_id": "req_example",
         "details": {
          "fields": [
           {
            "field": "name",
            "message": "Field required"
           }
          ]
         }
        }
       }
      }
     }
    },
    "tags": [
     "Webhook subscriptions"
    ],
    "x-corrconnect-errors": [
     {
      "status": 401,
      "code": "authentication_required",
      "when": "No console session cookie was sent."
     },
     {
      "status": 401,
      "code": "invalid_session",
      "when": "The session was signed out, replaced or never existed."
     },
     {
      "status": 401,
      "code": "session_expired",
      "when": "The session is older than its lifetime (12 hours by default)."
     },
     {
      "status": 403,
      "code": "email_verification_required",
      "when": "The account has not verified its email address yet."
     },
     {
      "status": 404,
      "code": "app_not_found",
      "when": "The app does not exist or belongs to another account."
     },
     {
      "status": 404,
      "code": "webhook_not_found",
      "when": "The app has no subscription with this ID."
     },
     {
      "status": 422,
      "code": "validation_failed",
      "when": "A field is missing, has the wrong type or is out of range; details.fields says which."
     }
    ],
    "description": "Newest first; limit defaults to 50 (1 to 100). Status and coded errors only, never payloads. Finished deliveries are kept for 30 days.",
    "x-corrconnect-auth": "session",
    "security": [
     {
      "CorrConnectSession": []
     }
    ]
   }
  }
 },
 "webhooks": {
  "activity.created": {
   "post": {
    "summary": "An athlete who granted activities:read added an activity",
    "description": "Sent to each active subscription for the app. Respond with any 2xx within 10 seconds. Anything else is tried again, up to 8 attempts in all over about two days. Payloads carry IDs only; fetch records with your token.",
    "parameters": [
     {
      "name": "CorrConnect-Signature",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of '<t>.' + raw body, keyed with the subscription secret>."
     },
     {
      "name": "CorrConnect-Event-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Same on every retry of one event; use it to drop duplicates."
     },
     {
      "name": "CorrConnect-Event-Type",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "activity.created, grant.revoked or webhook.test, the same as type in the body."
     },
     {
      "name": "CorrConnect-Delivery-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "One delivery of the event to one subscription (whd_...); the delivery log uses the same ID."
     },
     {
      "name": "CorrConnect-Delivery-Attempt",
      "in": "header",
      "required": true,
      "schema": {
       "type": "integer"
      },
      "description": "1 on the first try, up to 8."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "id",
         "type",
         "created_at",
         "app_id",
         "subject",
         "data"
        ],
        "properties": {
         "id": {
          "type": "string",
          "example": "evt_0123456789abcdef0123456789abcdef"
         },
         "type": {
          "type": "string",
          "enum": [
           "activity.created",
           "grant.revoked",
           "webhook.test"
          ]
         },
         "created_at": {
          "type": "string",
          "format": "date-time"
         },
         "app_id": {
          "type": "string"
         },
         "subject": {
          "anyOf": [
           {
            "type": "string"
           },
           {
            "type": "null"
           }
          ],
          "description": "Pairwise subject, the same value GET /v1/profile returns. Null for webhook.test."
         },
         "data": {
          "type": "object",
          "properties": {
           "activity_id": {
            "type": "string"
           }
          }
         }
        }
       },
       "example": {
        "id": "evt_0123456789abcdef0123456789abcdef",
        "type": "activity.created",
        "created_at": "2026-10-09T12:00:00+00:00",
        "app_id": "app_0123456789abcdef0123456789abcdef",
        "subject": "ccu_0123456789abcdef0123456789abcdef",
        "data": {
         "activity_id": "act_0123456789abcdef0123456789abcdef"
        }
       }
      }
     }
    },
    "responses": {
     "2XX": {
      "description": "Received."
     }
    }
   }
  },
  "grant.revoked": {
   "post": {
    "summary": "An athlete's access for this app ended",
    "description": "Sent to each active subscription for the app. Respond with any 2xx within 10 seconds. Anything else is tried again, up to 8 attempts in all over about two days. Payloads carry IDs only; fetch records with your token.",
    "parameters": [
     {
      "name": "CorrConnect-Signature",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of '<t>.' + raw body, keyed with the subscription secret>."
     },
     {
      "name": "CorrConnect-Event-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Same on every retry of one event; use it to drop duplicates."
     },
     {
      "name": "CorrConnect-Event-Type",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "activity.created, grant.revoked or webhook.test, the same as type in the body."
     },
     {
      "name": "CorrConnect-Delivery-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "One delivery of the event to one subscription (whd_...); the delivery log uses the same ID."
     },
     {
      "name": "CorrConnect-Delivery-Attempt",
      "in": "header",
      "required": true,
      "schema": {
       "type": "integer"
      },
      "description": "1 on the first try, up to 8."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "id",
         "type",
         "created_at",
         "app_id",
         "subject",
         "data"
        ],
        "properties": {
         "id": {
          "type": "string",
          "example": "evt_0123456789abcdef0123456789abcdef"
         },
         "type": {
          "type": "string",
          "enum": [
           "activity.created",
           "grant.revoked",
           "webhook.test"
          ]
         },
         "created_at": {
          "type": "string",
          "format": "date-time"
         },
         "app_id": {
          "type": "string"
         },
         "subject": {
          "anyOf": [
           {
            "type": "string"
           },
           {
            "type": "null"
           }
          ],
          "description": "Pairwise subject, the same value GET /v1/profile returns. Null for webhook.test."
         },
         "data": {
          "type": "object",
          "properties": {
           "activity_id": {
            "type": "string"
           }
          }
         }
        }
       },
       "example": {
        "id": "evt_0123456789abcdef0123456789abcdef",
        "type": "grant.revoked",
        "created_at": "2026-10-09T12:00:00+00:00",
        "app_id": "app_0123456789abcdef0123456789abcdef",
        "subject": "ccu_0123456789abcdef0123456789abcdef",
        "data": {}
       }
      }
     }
    },
    "responses": {
     "2XX": {
      "description": "Received."
     }
    }
   }
  },
  "webhook.test": {
   "post": {
    "summary": "You asked for a test delivery to one subscription",
    "description": "Sent only to the subscription you tested, after POST /v1/developer/apps/{app_id}/webhooks/{subscription_id}/test. subject is null. Retried like any other event.",
    "parameters": [
     {
      "name": "CorrConnect-Signature",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of '<t>.' + raw body, keyed with the subscription secret>."
     },
     {
      "name": "CorrConnect-Event-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Same on every retry of one event; use it to drop duplicates."
     },
     {
      "name": "CorrConnect-Event-Type",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "activity.created, grant.revoked or webhook.test, the same as type in the body."
     },
     {
      "name": "CorrConnect-Delivery-Id",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "One delivery of the event to one subscription (whd_...); the delivery log uses the same ID."
     },
     {
      "name": "CorrConnect-Delivery-Attempt",
      "in": "header",
      "required": true,
      "schema": {
       "type": "integer"
      },
      "description": "1 on the first try, up to 8."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "id",
         "type",
         "created_at",
         "app_id",
         "subject",
         "data"
        ],
        "properties": {
         "id": {
          "type": "string",
          "example": "evt_0123456789abcdef0123456789abcdef"
         },
         "type": {
          "type": "string",
          "enum": [
           "activity.created",
           "grant.revoked",
           "webhook.test"
          ]
         },
         "created_at": {
          "type": "string",
          "format": "date-time"
         },
         "app_id": {
          "type": "string"
         },
         "subject": {
          "anyOf": [
           {
            "type": "string"
           },
           {
            "type": "null"
           }
          ],
          "description": "Pairwise subject, the same value GET /v1/profile returns. Null for webhook.test."
         },
         "data": {
          "type": "object",
          "properties": {
           "activity_id": {
            "type": "string"
           }
          }
         }
        }
       },
       "example": {
        "id": "evt_0123456789abcdef0123456789abcdef",
        "type": "webhook.test",
        "created_at": "2026-10-09T12:00:00+00:00",
        "app_id": "app_0123456789abcdef0123456789abcdef",
        "subject": null,
        "data": {}
       }
      }
     }
    },
    "responses": {
     "2XX": {
      "description": "Received."
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "CorrConnectBearer": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "opaque",
    "description": "Scoped CorrConnect access token from authorization-code + S256 PKCE."
   },
   "CorrConnectSession": {
    "type": "apiKey",
    "in": "cookie",
    "name": "__Host-corrconnect_session",
    "description": "Console session from POST /v1/account/login. Outside production the cookie is corrconnect_session. Changes also need Origin set to the portal origin and X-CSRF-Token."
   }
  },
  "schemas": {
   "AppCreateRequest": {
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 80,
      "minLength": 2,
      "title": "Name",
      "description": "Shown to athletes on the consent screen (2 to 80 characters)."
     },
     "redirect_uris": {
      "items": {
       "type": "string"
      },
      "type": "array",
      "maxItems": 5,
      "minItems": 1,
      "title": "Redirect Uris",
      "description": "1 to 5 exact HTTPS callback URLs. No fragments, credentials or wildcards; plain-HTTP loopback works only on local development servers."
     }
    },
    "additionalProperties": false,
    "type": "object",
    "required": [
     "name",
     "redirect_uris"
    ],
    "title": "AppCreateRequest"
   },
   "Body_revoke_client_token_oauth_revoke_token_post": {
    "properties": {
     "token": {
      "anyOf": [
       {
        "type": "string",
        "maxLength": 512
       },
       {
        "type": "null"
       }
      ],
      "title": "Token",
      "description": "An access or refresh token, if you are not sending a Bearer header."
     },
     "token_type_hint": {
      "anyOf": [
       {
        "type": "string",
        "maxLength": 40
       },
       {
        "type": "null"
       }
      ],
      "title": "Token Type Hint",
      "description": "Optional: access_token or refresh_token. CorrConnect checks both either way."
     },
     "client_id": {
      "anyOf": [
       {
        "type": "string",
        "maxLength": 100
       },
       {
        "type": "null"
       }
      ],
      "title": "Client Id",
      "description": "Optional: when sent, a token from another app is ignored."
     }
    },
    "type": "object",
    "title": "Body_revoke_client_token_oauth_revoke_token_post"
   },
   "Body_token_exchange_oauth_token_post": {
    "properties": {
     "grant_type": {
      "type": "string",
      "title": "Grant Type",
      "description": "authorization_code or refresh_token."
     },
     "client_id": {
      "type": "string",
      "title": "Client Id",
      "description": "Your app's client_id."
     },
     "code": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "title": "Code",
      "description": "authorization_code only: the one-use code from your callback."
     },
     "redirect_uri": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "title": "Redirect Uri",
      "description": "authorization_code only: the same redirect_uri you sent to /oauth/authorize."
     },
     "code_verifier": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "title": "Code Verifier",
      "description": "authorization_code only: the PKCE verifier behind the code_challenge."
     },
     "refresh_token": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "title": "Refresh Token",
      "description": "refresh_token only: your current refresh token."
     },
     "scope": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "title": "Scope",
      "description": "refresh_token only, optional: ask for fewer scopes than the grant holds."
     }
    },
    "type": "object",
    "required": [
     "grant_type",
     "client_id"
    ],
    "title": "Body_token_exchange_oauth_token_post"
   },
   "LoginRequest": {
    "properties": {
     "email": {
      "type": "string",
      "maxLength": 254,
      "minLength": 3,
      "title": "Email",
      "description": "The account's email address."
     },
     "password": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "title": "Password",
      "description": "The account's password."
     }
    },
    "additionalProperties": false,
    "type": "object",
    "required": [
     "email",
     "password"
    ],
    "title": "LoginRequest"
   },
   "RegisterRequest": {
    "properties": {
     "email": {
      "type": "string",
      "maxLength": 254,
      "minLength": 3,
      "title": "Email",
      "description": "Your email address; it is stored in lower case."
     },
     "password": {
      "type": "string",
      "maxLength": 128,
      "minLength": 12,
      "title": "Password",
      "description": "12 to 128 characters using at least three of: uppercase, lowercase, number, symbol."
     }
    },
    "additionalProperties": false,
    "type": "object",
    "required": [
     "email",
     "password"
    ],
    "title": "RegisterRequest"
   },
   "WebhookCreateRequest": {
    "properties": {
     "callback_url": {
      "type": "string",
      "maxLength": 2000,
      "minLength": 10,
      "title": "Callback Url",
      "description": "Public HTTPS URL on port 443 (10 to 2,000 characters)."
     },
     "events": {
      "anyOf": [
       {
        "items": {
         "type": "string"
        },
        "type": "array",
        "maxItems": 5,
        "minItems": 1
       },
       {
        "type": "null"
       }
      ],
      "title": "Events",
      "description": "Optional. activity.created, grant.revoked or both (the default)."
     }
    },
    "additionalProperties": false,
    "type": "object",
    "required": [
     "callback_url"
    ],
    "title": "WebhookCreateRequest"
   }
  }
 },
 "x-corrconnect-rate-limits": {
  "free_tier": {
   "per_15_minutes": 100,
   "per_day": 10000,
   "scope": "per app"
  }
 }
}
