{
  "openapi": "3.1.0",
  "info": {
    "title": "ARA Labs API",
    "version": "1.0.0",
    "description": "ARA challenge definitions for eligible Gold members. Desktop login uses a proof-bound transaction and browser code confirmation, followed by sign-in on the main Academy website and a return to Labs. ARA bearer sessions last 12 hours. Provider credentials are never distributed to clients. Examples are synthetic."
  },
  "servers": [
    {
      "url": "https://labs.almeidaracingacademy.com"
    }
  ],
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://labs.almeidaracingacademy.com/docs"
  },
  "tags": [
    {
      "name": "Training plans",
      "description": "Read-only regular and weekly ARA challenge definitions."
    },
    {
      "name": "Authentication",
      "description": "Desktop login transactions and revocable ARA sessions."
    }
  ],
  "paths": {
    "/api/v1/garage61/training-plans": {
      "get": {
        "tags": [
          "Training plans"
        ],
        "security": [
          {
            "UserBearer": []
          }
        ],
        "description": "Requires an ARA session and current eligible Gold membership. Membership checks cache for up to 60 seconds; server-side authorization also protects cached definitions.",
        "operationId": "listTrainingPlans",
        "summary": "List ARA training plans",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "almeida-racing-academy"
              ],
              "default": "almeida-racing-academy"
            },
            "description": "Only the configured ARA team is allowed."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated provider plan summaries. Follow pagination until all items have been read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingPlanList"
                },
                "example": {
                  "team": "almeida-racing-academy",
                  "items": [
                    {
                      "id": "01KCRX69A5AYWJ5AXFXAT9DJNW",
                      "name": "Synthetic example \u2014 not an official challenge"
                    }
                  ],
                  "total": 1,
                  "limit": 100,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServiceError"
          },
          "502": {
            "$ref": "#/components/responses/ServiceError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/garage61/training-plans/{trainingPlanId}": {
      "get": {
        "tags": [
          "Training plans"
        ],
        "security": [
          {
            "UserBearer": []
          }
        ],
        "description": "Requires an ARA session and current eligible Gold membership. Membership checks cache for up to 60 seconds; server-side authorization also protects cached definitions.",
        "operationId": "getTrainingPlan",
        "summary": "Get a plan with simulator mappings and medal targets",
        "parameters": [
          {
            "name": "trainingPlanId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
            },
            "description": "Garage61 training plan ULID from the collection."
          },
          {
            "name": "team",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "almeida-racing-academy"
              ],
              "default": "almeida-racing-academy"
            },
            "description": "Only the configured ARA team is allowed."
          }
        ],
        "responses": {
          "200": {
            "description": "Provider plan content with ARA-added simulator mappings; no personal results. Example data is synthetic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingPlanDetail"
                },
                "example": {
                  "trainingPlanId": "01KCRX69A5AYWJ5AXFXAT9DJNW",
                  "team": "almeida-racing-academy",
                  "mode": "plan",
                  "trainingPlan": {
                    "id": "01KCRX69A5AYWJ5AXFXAT9DJNW",
                    "name": "Synthetic example \u2014 not an official challenge",
                    "content": [
                      {
                        "id": "example-challenge",
                        "type": "target_time",
                        "track": 69,
                        "cars": [
                          8
                        ],
                        "targets": [
                          {
                            "id": "bronze",
                            "label": "Bronze",
                            "lapTime": 108.5
                          },
                          {
                            "id": "silver",
                            "label": "Silver",
                            "lapTime": 108
                          },
                          {
                            "id": "gold",
                            "label": "Gold",
                            "lapTime": 107.5
                          }
                        ],
                        "trackInfo": {
                          "id": 69,
                          "name": "Example circuit",
                          "platform": "iracing",
                          "platform_id": "166"
                        },
                        "carInfos": [
                          {
                            "id": 8,
                            "name": "Example car",
                            "platform": "iracing",
                            "platform_id": "67"
                          }
                        ],
                        "result": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServiceError"
          },
          "502": {
            "$ref": "#/components/responses/ServiceError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/auth/transactions": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "createLoginTransaction",
        "summary": "Create a desktop login transaction",
        "description": "Generate a cryptographically random verifier locally; send only its S256 challenge. Display user_code in the app and open verification_uri in the system browser. No shared client secret is required.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_id": {
                    "type": "string",
                    "const": "ara-challenge-overlay"
                  },
                  "code_challenge_method": {
                    "type": "string",
                    "const": "S256"
                  },
                  "code_challenge": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                },
                "required": [
                  "client_id",
                  "code_challenge_method",
                  "code_challenge"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Transaction valid for ten minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transaction_id": {
                      "type": "string",
                      "pattern": "^[A-Za-z0-9_-]{43}$"
                    },
                    "user_code": {
                      "type": "string",
                      "pattern": "^[0-9]{8}$"
                    },
                    "verification_uri": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in": {
                      "type": "integer",
                      "const": 600
                    },
                    "interval": {
                      "type": "integer",
                      "const": 5
                    }
                  },
                  "required": [
                    "transaction_id",
                    "user_code",
                    "verification_uri",
                    "expires_in",
                    "interval"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or denied login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Session missing, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Gold membership required or browser origin denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Transaction expired or already redeemed. Start a new login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Respect Retry-After; wait 60 seconds if absent. Edge responses may have a non-JSON body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "503": {
            "description": "Service temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "redeemLoginTransaction",
        "summary": "Poll and redeem a completed login once",
        "description": "Poll at most once every five seconds with the original verifier. After 200, retain the token securely. If a successful response is lost, start a new login; replay cannot recover it.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "transaction_id": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  },
                  "code_verifier": {
                    "type": "string",
                    "minLength": 43,
                    "maxLength": 128,
                    "pattern": "^[A-Za-z0-9._~-]+$"
                  }
                },
                "required": [
                  "transaction_id",
                  "code_verifier"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A new opaque ARA session.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "pattern": "^ara_[A-Za-z0-9_-]{43}$"
                    },
                    "token_type": {
                      "type": "string",
                      "const": "Bearer"
                    },
                    "expires_in": {
                      "type": "integer",
                      "const": 43200
                    },
                    "scope": {
                      "type": "string",
                      "const": "training-plans:read"
                    }
                  },
                  "required": [
                    "access_token",
                    "token_type",
                    "expires_in",
                    "scope"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "The member has not completed browser login.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "authorization_pending"
                    },
                    "interval": {
                      "type": "integer",
                      "const": 5
                    }
                  },
                  "required": [
                    "status",
                    "interval"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or denied login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Session missing, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Gold membership required or browser origin denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Transaction expired or already redeemed. Start a new login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Respect Retry-After; wait 60 seconds if absent. Edge responses may have a non-JSON body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "503": {
            "description": "Service temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/session": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "operationId": "getSession",
        "summary": "Check the session and Gold access",
        "security": [
          {
            "UserBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Active eligible session.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authenticated": {
                      "type": "boolean",
                      "const": true
                    },
                    "entitlements": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "gold"
                        ]
                      }
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "authenticated",
                    "entitlements",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or denied login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Session missing, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Gold membership required or browser origin denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Transaction expired or already redeemed. Start a new login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Respect Retry-After; wait 60 seconds if absent. Edge responses may have a non-JSON body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "503": {
            "description": "Service temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Authentication"
        ],
        "operationId": "revokeSession",
        "summary": "Revoke this app session",
        "description": "Also remove local credentials. Does not sign the member out of their Academy browser session.",
        "security": [
          {
            "UserBearer": []
          }
        ],
        "responses": {
          "204": {
            "description": "Session revoked or already absent."
          },
          "400": {
            "description": "Invalid request or denied login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Session missing, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Gold membership required or browser origin denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Transaction expired or already redeemed. Start a new login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Respect Retry-After; wait 60 seconds if absent. Edge responses may have a non-JSON body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "503": {
            "description": "Service temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "UserBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Opaque ARA bearer token from POST /api/v1/auth/token. Never send an Outseta token, client secret or Garage61 key."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "requestId": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": true
      },
      "CatalogItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "platform": {
            "type": "string"
          },
          "platform_id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "Simulator ID; normalize numeric strings before matching iRacing."
          }
        },
        "additionalProperties": true
      },
      "MedalTarget": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "lapTime": {
            "type": "number",
            "description": "Target lap time in seconds."
          }
        },
        "additionalProperties": true
      },
      "PlanContent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "position": {
            "type": "integer"
          },
          "track": {
            "type": "integer"
          },
          "cars": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Explicitly empty means all cars. Missing/unmapped cars must not be treated as unrestricted."
          },
          "targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MedalTarget"
            }
          },
          "trackInfo": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CatalogItem"
              },
              {
                "type": "null"
              }
            ],
            "description": "May be null or omitted when the track mapping is unavailable."
          },
          "carInfos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogItem"
            },
            "description": "May contain fewer entries than cars when mappings are missing."
          },
          "result": {
            "type": "null"
          }
        },
        "required": [
          "carInfos",
          "result"
        ],
        "additionalProperties": true
      },
      "TrainingPlan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanContent"
            }
          }
        },
        "required": [
          "id",
          "content"
        ],
        "additionalProperties": true
      },
      "TrainingPlanSummary": {
        "type": "object",
        "description": "Provider-owned summary; additional metadata is preserved. Use the detail route for enriched content.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "TrainingPlanList": {
        "type": "object",
        "properties": {
          "team": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrainingPlanSummary"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        },
        "required": [
          "team",
          "items",
          "total",
          "limit",
          "offset"
        ],
        "additionalProperties": true
      },
      "TrainingPlanDetail": {
        "type": "object",
        "properties": {
          "trainingPlanId": {
            "type": "string"
          },
          "team": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "const": "plan"
          },
          "trainingPlan": {
            "$ref": "#/components/schemas/TrainingPlan"
          }
        },
        "required": [
          "trainingPlanId",
          "team",
          "mode",
          "trainingPlan"
        ],
        "additionalProperties": true
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Invalid team, pagination or training plan ID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "User bearer token is missing or invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "The upstream training plan was not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Only GET and OPTIONS are accepted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Request or provider rate limit exceeded. Respect Retry-After; wait 60 seconds if absent. Edge responses may have a non-JSON body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Delay in seconds before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "ServiceError": {
        "description": "Service or provider error; use bounded retries with jitter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "Service temporarily unavailable or release paused.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Challenge API is not available yet",
              "requestId": "00000000-0000-4000-8000-000000000000"
            }
          }
        }
      },
      "Timeout": {
        "description": "Provider request exceeded its time budget.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Eligible Gold membership required.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
