{
  "openapi": "3.1.0",
  "info": {
    "title": "The Student Life API",
    "version": "1.0.0",
    "summary": "Student cities, events, ticket prices and partner deals.",
    "description": "Open, keyless JSON API of The Student Life (jointhestudentlife.com): the cities where we run student communities, their guides, upcoming events with ticket prices and status, partner deals and pricing. Read endpoints need no authentication. Data refreshes whenever the website does. For AI agents, the same data is available as an MCP server at https://jointhestudentlife.com/mcp. Versioning and deprecation policy: the version is in the path (/api/v1); inside v1 we aim to only add things; a breaking change ships as /api/v2 and is announced on https://jointhestudentlife.com/developers#versioning before the old version changes, with Deprecation (RFC 9745) and, once a date is set, Sunset (RFC 8594) headers on affected endpoints. Rate limits are best effort per client, reported in RateLimit and X-RateLimit headers. Pagination is cursor based. Sandbox: POST /api/contact accepts dry_run to validate without sending.",
    "contact": {
      "name": "The Student Life",
      "email": "support@jointhestudentlife.com",
      "url": "https://jointhestudentlife.com/developers"
    },
    "x-deprecation-policy": "https://jointhestudentlife.com/developers#versioning"
  },
  "servers": [
    {
      "url": "https://jointhestudentlife.com"
    }
  ],
  "externalDocs": {
    "description": "For AI agents and developers",
    "url": "https://jointhestudentlife.com/developers"
  },
  "security": [],
  "tags": [
    {
      "name": "cities",
      "description": "Cities and city guides"
    },
    {
      "name": "events",
      "description": "Events and tickets"
    },
    {
      "name": "deals",
      "description": "Partner deals and pricing"
    },
    {
      "name": "contact",
      "description": "Messages to the team"
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "List the endpoints",
        "tags": [
          "cities"
        ],
        "responses": {
          "200": {
            "description": "Index of endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/cities": {
      "get": {
        "operationId": "searchCities",
        "summary": "Search cities",
        "description": "Cities where The Student Life runs communities and events, with links to each city guide, festival page and Halloween page.",
        "tags": [
          "cities"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "City or country name, partial is fine.",
            "schema": {
              "type": "string"
            },
            "example": "berlin"
          },
          {
            "name": "country",
            "in": "query",
            "description": "Country name or two letter code.",
            "schema": {
              "type": "string"
            },
            "example": "DE"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            },
            "description": "Page size."
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching cities",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CityList"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/cities/{slug}": {
      "get": {
        "operationId": "getCityGuide",
        "summary": "Get a city guide",
        "description": "Costs, neighbourhoods, universities, housing tips, places and upcoming events for one city.",
        "tags": [
          "cities"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "City slug from searchCities, or a city name.",
            "schema": {
              "type": "string"
            },
            "example": "lisbon"
          }
        ],
        "responses": {
          "200": {
            "description": "City guide",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CityGuide"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "List events",
        "description": "Halloween parties, Welcome Festivals, semester kickoff parties and orientation weeks, with ticket prices per round, early access, status and the buy link.",
        "tags": [
          "events"
        ],
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "description": "City slug or name.",
            "schema": {
              "type": "string"
            },
            "example": "paris"
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "halloween",
                "welcome_festival",
                "semester_kickoff",
                "orientation_week"
              ]
            }
          },
          {
            "name": "upcoming",
            "in": "query",
            "description": "Only events that have not ended. Default true.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            },
            "description": "Page size."
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Events",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/events/{id}": {
      "get": {
        "operationId": "getEvent",
        "summary": "Get one event",
        "tags": [
          "events"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Event id from listEvents.",
            "schema": {
              "type": "string"
            },
            "example": "berlin-hw26"
          }
        ],
        "responses": {
          "200": {
            "description": "Event",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/partners": {
      "get": {
        "operationId": "listPartnerDeals",
        "summary": "List student deals",
        "tags": [
          "deals"
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "eSIM"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Partner deals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    },
                    "partners": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Partner"
                      }
                    },
                    "page": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/pricing": {
      "get": {
        "operationId": "getPricing",
        "summary": "Get pricing",
        "description": "What is free, Halloween ticket prices per city and round, packages, partner deals and refund and fee policy.",
        "tags": [
          "deals"
        ],
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pricing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "operationId": "sendMessage",
        "summary": "Send a message to the team",
        "description": "The endpoint behind the website forms. Emails support@jointhestudentlife.com with reply to set to the sender. Only send with the person's consent and their own details. Meant for server side callers: browser pages on other sites get 403. About 20 messages per minute per client.",
        "tags": [
          "contact"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "message"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 200
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 6000
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "city": {
                    "type": "string"
                  },
                  "form_type": {
                    "type": "string",
                    "description": "Which form this is, e.g. agent-question."
                  },
                  "dry_run": {
                    "type": "boolean",
                    "description": "Sandbox: validate the message and stop before anything is sent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "dry_run": {
                      "type": "boolean"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Key": {
                "$ref": "#/components/headers/Idempotency-Key"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            }
          },
          "400": {
            "description": "Missing or invalid fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "405": {
            "description": "Only POST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "502": {
            "description": "Could not send",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "503": {
            "description": "Not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "403": {
            "description": "A browser page on another site (forbidden_origin)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "409": {
            "description": "A request with this Idempotency-Key is still being processed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency-Key reused with a different body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactError"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A fresh unique string per message, for example a UUID. If a retry with the same key and the same body arrives after the first message was sent, the answer is 200 with Idempotent-Replayed: true and nothing is sent again; the same key with a different body gets 422. Best effort: kept 24 hours in server memory, only successful sends are remembered.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ]
      }
    }
  },
  "components": {
    "responses": {
      "Error": {
        "description": "Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "API-Version": {
            "$ref": "#/components/headers/API-Version"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "bad_request",
                  "not_found",
                  "method_not_allowed",
                  "internal_error",
                  "rate_limited"
                ]
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "ContactError": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "code": {
            "type": "string"
          }
        }
      },
      "CityCard": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "country_code": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "festival_name": {
            "type": "string"
          },
          "city_guide_url": {
            "type": "string",
            "format": "uri"
          },
          "festival_url": {
            "type": "string",
            "format": "uri"
          },
          "halloween_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "CityList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "cities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CityCard"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as cursor to get the next page; null on the last page."
          }
        }
      },
      "CityGuide": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CityCard"
          },
          {
            "type": "object",
            "properties": {
              "about": {
                "type": "string"
              },
              "monthly_costs": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  }
                }
              },
              "universities": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              },
              "upcoming_events": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          }
        ]
      },
      "TicketOption": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          }
        }
      },
      "Event": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "halloween",
              "welcome_festival",
              "semester_kickoff",
              "orientation_week"
            ]
          },
          "name": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "city_slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "start": {
            "type": "string"
          },
          "end": {
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ]
          },
          "venue": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "age_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "enum": [
              "upcoming",
              "past"
            ]
          },
          "tickets": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "on_sale",
                  "opens_soon",
                  "coming_soon",
                  "see_page",
                  "ended"
                ],
                "description": "opens_soon: published, but sales open at sales_open_at."
              },
              "options": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TicketOption"
                }
              },
              "early_access": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "buy_url": {
                "type": "string",
                "format": "uri"
              },
              "note": {
                "type": "string"
              },
              "sales_open_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When ticket sales open, for status opens_soon."
              }
            }
          },
          "waitlist_open": {
            "type": "boolean"
          },
          "whatsapp_group": {
            "type": [
              "string",
              "null"
            ]
          },
          "entry_rule": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "EventList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as cursor to get the next page; null on the last page."
          }
        }
      },
      "Partner": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "deal": {
            "type": "string"
          },
          "code": {
            "type": [
              "string",
              "null"
            ]
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit": {
        "description": "Current window: limit, remaining and seconds until reset (draft-ietf-httpapi-ratelimit-headers).",
        "schema": {
          "type": "string"
        },
        "example": "limit=120, remaining=119, reset=60"
      },
      "RateLimit-Policy": {
        "description": "Quota policy.",
        "schema": {
          "type": "string"
        },
        "example": "120;w=60"
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix time when the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait after a 429.",
        "schema": {
          "type": "integer"
        }
      },
      "API-Version": {
        "description": "Major version that served the response.",
        "schema": {
          "type": "string"
        },
        "example": "1"
      },
      "Idempotency-Key": {
        "description": "Echo of the Idempotency-Key request header.",
        "schema": {
          "type": "string"
        }
      },
      "Idempotent-Replayed": {
        "description": "true when the response is the stored answer to an earlier request with the same Idempotency-Key.",
        "schema": {
          "type": "string"
        }
      }
    },
    "parameters": {
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "Opaque cursor: the next_cursor of the previous page. Leave out for the first page.",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}
