{
  "openapi": "3.1.0",
  "info": {
    "title": "BentoCal API",
    "version": "1.0.0",
    "summary": "Connect Google and Microsoft calendars, read free/busy, find available times and write events.",
    "description": "BentoCal connects your application to your users' Google and Microsoft calendars: one API for connecting accounts, reading free/busy, finding available times and writing events, plus webhooks when things change.\n\nSend your API key as a bearer token (`Authorization: Bearer bc_test_au_…`) to your region's base URL, `https://api.bentocal.com/{region}/v1`. Available regions: `au` (Australia). Bodies are JSON with `snake_case` fields, and users are identified by your own ids.\n\n## Guides\n\n- [Quickstart](https://docs.bentocal.com/OsBzHeSwDqR9YReehwGd-quickstart): connect a calendar and find a free time in a few calls.\n- [Authentication and API keys](https://docs.bentocal.com/g4XiUmHGl8veMKuen5eb-authentication-and-api-keys) · [Regions and base URLs](https://docs.bentocal.com/hoUIJFmGs8vpEMI3WsTh-regions-and-base-urls) · [Development and production](https://docs.bentocal.com/d9gU6KRbUbHStfAgh516-development-and-production)\n- [Connecting calendars](https://docs.bentocal.com/dGnYRxRVhvXu1eYkZULk-connecting-calendars) · [Free/busy and availability](https://docs.bentocal.com/66bDsJMvD0pUXqb9Qz1J-freebusy-and-availability) · [Writing events](https://docs.bentocal.com/UZN4o7TaPfj2pEC1Ef5C-writing-events)\n- [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks): event types, signature verification and retries.\n- [Managing users and connections](https://docs.bentocal.com/Jgwe7ihtNphFhMYzB9dV-managing-users-and-connections)\n- [Errors](https://docs.bentocal.com/VFBWX8RKJ1x0lZgvrLj9-errors) · [Limits and rate limits](https://docs.bentocal.com/w76yoyDhffnQKN3xTftm-limits-and-rate-limits)",
    "contact": {
      "name": "BentoCal",
      "url": "https://bentocal.com"
    }
  },
  "servers": [
    {
      "url": "https://api.bentocal.com/{region}/v1",
      "description": "Your application's region",
      "variables": {
        "region": {
          "default": "au",
          "enum": [
            "au"
          ],
          "description": "The region your application lives in."
        }
      }
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Connect sessions",
      "description": "Send a user through connecting a Google or Microsoft account."
    },
    {
      "name": "Users",
      "description": "Your end users and their connections, by your own user id."
    },
    {
      "name": "Connections",
      "description": "One connected calendar account."
    },
    {
      "name": "Scheduling",
      "description": "Free/busy and available times across users."
    },
    {
      "name": "Events",
      "description": "Write events to a user's calendar under your own event id."
    },
    {
      "name": "Webhooks",
      "description": "Events BentoCal sends to your webhook URL."
    }
  ],
  "paths": {
    "/connect_sessions": {
      "post": {
        "operationId": "createConnectSession",
        "tags": [
          "Connect sessions"
        ],
        "summary": "Create a connect session",
        "description": "Starts connecting (or relinking) a calendar account for one of your users. Send the user to the returned `url`: the hosted page lets them pick Google or Microsoft and sign in.\n\nWhen the flow ends the user is redirected (303) to `redirect_uri` with `connect_session_id`, `status` (`success`, `denied` or `error`) and, on error, `error_code` in the query string. Confirm the outcome with `GET /connect_sessions/{connect_session_id}` (or the `connection.created` webhook) rather than trusting the query string.\n\nSessions expire after 30 minutes. Pass `connection_id` to relink a connection whose status is `relink_required`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectSessionRequest"
              },
              "example": {
                "user_id": "user_123",
                "redirect_uri": "https://app.example.com/calendar/connected"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The session was created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectSession"
                },
                "example": {
                  "id": "cs_9vB2nR7kLp4QxT1w",
                  "status": "pending",
                  "user_id": "user_123",
                  "url": "https://api.bentocal.com/au/connect/cs_9vB2nR7kLp4QxT1w",
                  "expires_at": "2026-10-15T00:30:00Z",
                  "connection": null,
                  "error_code": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/connect_sessions/{connect_session_id}": {
      "get": {
        "operationId": "getConnectSession",
        "tags": [
          "Connect sessions"
        ],
        "summary": "Get a connect session",
        "description": "Returns the session and, once it is `completed`, the connection it made or relinked.",
        "parameters": [
          {
            "name": "connect_session_id",
            "in": "path",
            "required": true,
            "description": "Connect session id (`cs_…`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectSession"
                },
                "example": {
                  "id": "cs_9vB2nR7kLp4QxT1w",
                  "status": "completed",
                  "user_id": "user_123",
                  "url": "https://api.bentocal.com/au/connect/cs_9vB2nR7kLp4QxT1w",
                  "expires_at": "2026-10-15T00:30:00Z",
                  "connection": {
                    "id": "conn_4kT9pQ2vXa7LmN3s",
                    "user_id": "user_123",
                    "provider": "google",
                    "email": "sam@example.com",
                    "display_name": "Sam Lee",
                    "status": "active",
                    "primary_calendar_id": "sam@example.com",
                    "connected_at": "2026-10-01T03:12:45Z",
                    "last_used_at": "2026-10-14T22:01:09Z"
                  },
                  "error_code": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/users/{user_id}": {
      "get": {
        "operationId": "getUser",
        "tags": [
          "Users"
        ],
        "summary": "Get a user",
        "description": "Returns the user and all their connections, including revoked ones. A user exists once they have connected a calendar.",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "Your identifier for the end user. URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The user.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "user_id": "user_123",
                  "default_connection_id": "conn_4kT9pQ2vXa7LmN3s",
                  "connections": [
                    {
                      "id": "conn_4kT9pQ2vXa7LmN3s",
                      "user_id": "user_123",
                      "provider": "google",
                      "email": "sam@example.com",
                      "display_name": "Sam Lee",
                      "status": "active",
                      "primary_calendar_id": "sam@example.com",
                      "connected_at": "2026-10-01T03:12:45Z",
                      "last_used_at": "2026-10-14T22:01:09Z"
                    }
                  ],
                  "created_at": "2026-10-01T03:12:45Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateUser",
        "tags": [
          "Users"
        ],
        "summary": "Update a user",
        "description": "Sets the default connection: the one events are written to when an event write names no `connection_id`.",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "Your identifier for the end user. URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateUserRequest"
              },
              "example": {
                "default_connection_id": "conn_4kT9pQ2vXa7LmN3s"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated user.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "user_id": "user_123",
                  "default_connection_id": "conn_4kT9pQ2vXa7LmN3s",
                  "connections": [
                    {
                      "id": "conn_4kT9pQ2vXa7LmN3s",
                      "user_id": "user_123",
                      "provider": "google",
                      "email": "sam@example.com",
                      "display_name": "Sam Lee",
                      "status": "active",
                      "primary_calendar_id": "sam@example.com",
                      "connected_at": "2026-10-01T03:12:45Z",
                      "last_used_at": "2026-10-14T22:01:09Z"
                    }
                  ],
                  "created_at": "2026-10-01T03:12:45Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteUser",
        "tags": [
          "Users"
        ],
        "summary": "Delete a user",
        "description": "Erases the user: revokes every connection at the provider (best effort), wipes their tokens and deletes their event links and the user record. Events already on their calendars are left in place. A `connection.revoked` webhook is sent for each connection that was not already revoked.",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "Your identifier for the end user. URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The user was deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/connections/{connection_id}": {
      "get": {
        "operationId": "getConnection",
        "tags": [
          "Connections"
        ],
        "summary": "Get a connection",
        "description": "Returns one connection.",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "description": "Connection id (`conn_…`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The connection.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Connection"
                },
                "example": {
                  "id": "conn_4kT9pQ2vXa7LmN3s",
                  "user_id": "user_123",
                  "provider": "google",
                  "email": "sam@example.com",
                  "display_name": "Sam Lee",
                  "status": "active",
                  "primary_calendar_id": "sam@example.com",
                  "connected_at": "2026-10-01T03:12:45Z",
                  "last_used_at": "2026-10-14T22:01:09Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteConnection",
        "tags": [
          "Connections"
        ],
        "summary": "Revoke a connection",
        "description": "Revokes access at the provider (best effort), wipes the stored tokens and forgets the event links made through it (the events stay on the calendar). The connection stays readable with status `revoked`. If it was the user's default, their oldest other active connection becomes the default. Revoking an already revoked connection is a success.",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "description": "Connection id (`conn_…`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The connection was revoked.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/connections/{connection_id}/calendars": {
      "get": {
        "operationId": "listCalendars",
        "tags": [
          "Connections"
        ],
        "summary": "List calendars",
        "description": "Lists the calendars on the connected account, read live from the provider.",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "description": "Connection id (`conn_…`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The calendars.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Calendar"
                  }
                },
                "example": [
                  {
                    "id": "sam@example.com",
                    "name": "Sam Lee",
                    "primary": true,
                    "can_write": true,
                    "time_zone": "Australia/Sydney"
                  }
                ]
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/ProviderUnavailable"
          }
        }
      }
    },
    "/free_busy": {
      "post": {
        "operationId": "getFreeBusy",
        "tags": [
          "Scheduling"
        ],
        "summary": "Get free/busy",
        "description": "Returns busy time for up to 100 users over at most 62 days. Busy means the provider's busy, tentative and out-of-office statuses; free and working-elsewhere time does not count.\n\nA user whose calendars can't be read is reported in `errors` and the rest still succeed, so this returns 200 even when every user fails.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FreeBusyRequest"
              },
              "example": {
                "user_ids": [
                  "user_123",
                  "user_456"
                ],
                "start": "2026-10-20T00:00:00Z",
                "end": "2026-10-27T00:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Busy time per user.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FreeBusyResponse"
                },
                "example": {
                  "users": [
                    {
                      "user_id": "user_123",
                      "busy": [
                        {
                          "start": "2026-10-20T01:00:00Z",
                          "end": "2026-10-20T02:30:00Z"
                        }
                      ]
                    }
                  ],
                  "errors": [
                    {
                      "user_id": "user_456",
                      "connection_id": "conn_7rD1sW8eYq2KjH6b",
                      "code": "relink_required",
                      "message": "The calendar connection must be re-linked by the user."
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/availability": {
      "post": {
        "operationId": "getAvailability",
        "tags": [
          "Scheduling"
        ],
        "summary": "Find available times",
        "description": "Finds times when the participants are free inside the given periods (up to 100 participants and 200 periods spanning at most 62 days).\n\nWith `format: \"slots\"` it returns bookable slots that fit inside a period, start on the `slot_interval_minutes` grid from 00:00 UTC and leave every required participant free for the whole slot plus buffers. With `format: \"periods\"` it returns maximal free intervals instead.\n\nParticipants whose calendars can't be read are reported in `errors` and left out. With `required: \"all\"` any such error means no results.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AvailabilityRequest"
              },
              "example": {
                "participants": [
                  {
                    "user_id": "user_123"
                  },
                  {
                    "user_id": "user_456"
                  }
                ],
                "required": "all",
                "periods": [
                  {
                    "start": "2026-10-20T22:00:00Z",
                    "end": "2026-10-21T07:00:00Z"
                  }
                ],
                "slot_duration_minutes": 30,
                "buffer_after_minutes": 10
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Available times.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityResponse"
                },
                "example": {
                  "slots": [
                    {
                      "start": "2026-10-20T22:00:00Z",
                      "end": "2026-10-20T22:30:00Z",
                      "user_ids": [
                        "user_123",
                        "user_456"
                      ]
                    },
                    {
                      "start": "2026-10-20T22:30:00Z",
                      "end": "2026-10-20T23:00:00Z",
                      "user_ids": [
                        "user_123",
                        "user_456"
                      ]
                    }
                  ],
                  "errors": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/users/{user_id}/events/{event_id}": {
      "put": {
        "operationId": "upsertEvent",
        "tags": [
          "Events"
        ],
        "summary": "Create or update an event",
        "description": "Writes an event to the user's calendar under your own `event_id`. The first write creates it; later writes with the same id update it, so retries are safe.\n\nIt goes to the default connection's primary calendar unless `connection_id` or `calendar_id` says otherwise. If a later write targets a different calendar, the event moves. If the event was deleted at the provider, it is created again.",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "Your identifier for the end user. URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Your own identifier for the event (for example your booking id). URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertEventRequest"
              },
              "example": {
                "summary": "Intro call",
                "description": "Booked through Example App.",
                "start": "2026-10-20T23:00:00Z",
                "end": "2026-10-20T23:30:00Z",
                "time_zone": "Australia/Sydney",
                "location": "https://meet.example.com/abc"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The event was updated.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                },
                "example": {
                  "event_id": "booking_8812",
                  "connection_id": "conn_4kT9pQ2vXa7LmN3s",
                  "calendar_id": "sam@example.com",
                  "provider_event_id": "5q2hb7n1k8v3c0d9"
                }
              }
            }
          },
          "201": {
            "description": "The event was created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                },
                "example": {
                  "event_id": "booking_8812",
                  "connection_id": "conn_4kT9pQ2vXa7LmN3s",
                  "calendar_id": "sam@example.com",
                  "provider_event_id": "5q2hb7n1k8v3c0d9"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/ProviderUnavailable"
          }
        }
      },
      "delete": {
        "operationId": "deleteEvent",
        "tags": [
          "Events"
        ],
        "summary": "Delete an event",
        "description": "Deletes the event from the calendar and forgets the link. Idempotent: an unknown `event_id`, or an event already gone at the provider, is a success. If the connection needs relinking the link is kept and `relink_required` returned, so you can retry later.",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "Your identifier for the end user. URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Your own identifier for the event (for example your booking id). URL-encode it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The event was deleted (or did not exist).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/ProviderUnavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "connection.created": {
      "post": {
        "operationId": "webhook_connection_created",
        "tags": [
          "Webhooks"
        ],
        "summary": "A user connected a calendar account",
        "description": "Sent to your application's webhook URL when it subscribes to `connection.created`. See [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks) for signing and retries.",
        "parameters": [
          {
            "name": "BentoCal-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. During a secret rotation there is a second `v1=` entry.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "BentoCal-Event-Id",
            "in": "header",
            "required": true,
            "description": "Same as the body `id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionCreatedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status acknowledges the event."
          },
          "default": {
            "description": "Anything else (including redirects and timeouts) is retried."
          }
        }
      }
    },
    "connection.relinked": {
      "post": {
        "operationId": "webhook_connection_relinked",
        "tags": [
          "Webhooks"
        ],
        "summary": "A user relinked a connection",
        "description": "Sent to your application's webhook URL when it subscribes to `connection.relinked`. See [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks) for signing and retries.",
        "parameters": [
          {
            "name": "BentoCal-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. During a secret rotation there is a second `v1=` entry.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "BentoCal-Event-Id",
            "in": "header",
            "required": true,
            "description": "Same as the body `id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionRelinkedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status acknowledges the event."
          },
          "default": {
            "description": "Anything else (including redirects and timeouts) is retried."
          }
        }
      }
    },
    "connection.relink_required": {
      "post": {
        "operationId": "webhook_connection_relink_required",
        "tags": [
          "Webhooks"
        ],
        "summary": "A connection needs relinking (the provider stopped accepting its grant)",
        "description": "Sent to your application's webhook URL when it subscribes to `connection.relink_required`. See [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks) for signing and retries.",
        "parameters": [
          {
            "name": "BentoCal-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. During a secret rotation there is a second `v1=` entry.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "BentoCal-Event-Id",
            "in": "header",
            "required": true,
            "description": "Same as the body `id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionRelinkRequiredEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status acknowledges the event."
          },
          "default": {
            "description": "Anything else (including redirects and timeouts) is retried."
          }
        }
      }
    },
    "connection.revoked": {
      "post": {
        "operationId": "webhook_connection_revoked",
        "tags": [
          "Webhooks"
        ],
        "summary": "A connection was revoked",
        "description": "Sent to your application's webhook URL when it subscribes to `connection.revoked`. See [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks) for signing and retries.",
        "parameters": [
          {
            "name": "BentoCal-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. During a secret rotation there is a second `v1=` entry.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "BentoCal-Event-Id",
            "in": "header",
            "required": true,
            "description": "Same as the body `id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionRevokedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status acknowledges the event."
          },
          "default": {
            "description": "Anything else (including redirects and timeouts) is retried."
          }
        }
      }
    },
    "calendar.changed": {
      "post": {
        "operationId": "webhook_calendar_changed",
        "tags": [
          "Webhooks"
        ],
        "summary": "Events changed on a connection's primary calendar",
        "description": "Sent to your application's webhook URL when it subscribes to `calendar.changed`. See [Webhooks](https://docs.bentocal.com/AvgpvMZwJvhA0vtsrTY4-webhooks) for signing and retries.",
        "parameters": [
          {
            "name": "BentoCal-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. During a secret rotation there is a second `v1=` entry.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "BentoCal-Event-Id",
            "in": "header",
            "required": true,
            "description": "Same as the body `id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalendarChangedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status acknowledges the event."
          },
          "default": {
            "description": "Anything else (including redirects and timeouts) is retried."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateConnectSessionRequest": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Your own identifier for the end user (1–255 characters). The prefix `bentocal-test:` is reserved."
          },
          "redirect_uri": {
            "type": "string",
            "format": "uri",
            "description": "Where the user is sent when the flow ends. Must exactly match one of the application's registered redirect URIs."
          },
          "provider": {
            "description": "Offer only this provider on the connect page. The page still shows, so the user can accept the terms.",
            "type": "string",
            "enum": [
              "google",
              "microsoft"
            ]
          },
          "connection_id": {
            "description": "Relink this existing connection (it must belong to `user_id`). The user must sign in to the same provider account.",
            "type": "string"
          }
        },
        "required": [
          "user_id",
          "redirect_uri"
        ]
      },
      "ConnectSession": {
        "description": "A one-time link that takes an end user through connecting a calendar account.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Connect session id (`cs_…`)."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "redirected",
              "completed",
              "denied",
              "error",
              "expired"
            ],
            "description": "`pending`: created, not opened. `redirected`: the user went to the provider. `completed`: a connection was made or relinked. `denied`: the user declined at the provider. `error`: see `error_code`. `expired`: not finished in time."
          },
          "user_id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The hosted connect page. Send the user here."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the session expires (30 minutes after creation)."
          },
          "connection": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Connection"
              },
              {
                "type": "null"
              }
            ],
            "description": "The connection made or relinked, once `status` is `completed`."
          },
          "error_code": {
            "description": "Why the session ended in `error`: `account_mismatch` (a relink signed in to a different account), `connection_limit_reached`, `connection_not_found` (the connection being relinked was deleted), `session_expired` or `internal_error`. New codes may be added.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "status",
          "user_id",
          "url",
          "expires_at",
          "connection",
          "error_code"
        ]
      },
      "Connection": {
        "description": "One end user's link to one Google or Microsoft account.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Connection id (`conn_…`)."
          },
          "user_id": {
            "type": "string",
            "description": "Your identifier for the end user who owns the connection."
          },
          "provider": {
            "type": "string",
            "enum": [
              "google",
              "microsoft"
            ]
          },
          "email": {
            "description": "The provider account's email address.",
            "type": [
              "string",
              "null"
            ]
          },
          "display_name": {
            "description": "The provider account's display name.",
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "relink_required",
              "revoked"
            ],
            "description": "`active`: usable. `relink_required`: the provider stopped accepting the stored grant; send the user through a connect session with `connection_id` to fix it. `revoked`: disconnected for good."
          },
          "primary_calendar_id": {
            "description": "The provider's id for the account's primary calendar.",
            "type": [
              "string",
              "null"
            ]
          },
          "connected_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the connection was first made."
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "description": "When the API last used the connection."
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "user_id",
          "provider",
          "email",
          "display_name",
          "status",
          "primary_calendar_id",
          "connected_at",
          "last_used_at"
        ]
      },
      "User": {
        "description": "An end user, identified by your own id, and their connections.",
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "default_connection_id": {
            "description": "The connection events are written to when an event write names no `connection_id`.",
            "type": [
              "string",
              "null"
            ]
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Connection"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the user first connected a calendar."
          }
        },
        "required": [
          "user_id",
          "default_connection_id",
          "connections",
          "created_at"
        ]
      },
      "UpdateUserRequest": {
        "type": "object",
        "properties": {
          "default_connection_id": {
            "type": "string",
            "description": "An `active` connection of this user."
          }
        },
        "required": [
          "default_connection_id"
        ]
      },
      "Calendar": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The provider's calendar id. Use it in `calendar_ids` and event writes."
          },
          "name": {
            "type": "string"
          },
          "primary": {
            "type": "boolean",
            "description": "True for the account's primary calendar."
          },
          "can_write": {
            "type": "boolean",
            "description": "True when events can be written to this calendar."
          },
          "time_zone": {
            "description": "IANA time zone of the calendar, when the provider reports one.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "primary",
          "can_write",
          "time_zone"
        ]
      },
      "FreeBusyRequest": {
        "type": "object",
        "properties": {
          "user_ids": {
            "minItems": 1,
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Your own identifier for the end user (1–255 characters). The prefix `bentocal-test:` is reserved."
            }
          },
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the range."
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "End of the range: after `start` and at most 62 days later."
          },
          "calendar_ids": {
            "description": "Calendars to read per user id: calendar ids from any of the user's connections, `primary` (the default connection's primary calendar) or an account's email address (its primary calendar). A user not listed is read on every active connection's primary calendar.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "user_ids",
          "start",
          "end"
        ]
      },
      "FreeBusyResponse": {
        "type": "object",
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "user_id": {
                  "type": "string"
                },
                "busy": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TimeInterval"
                  },
                  "description": "Busy, tentative and out-of-office time, merged and sorted."
                }
              },
              "required": [
                "user_id",
                "busy"
              ]
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantError"
            },
            "description": "Users whose calendars could not be read. They are left out of `users`."
          }
        },
        "required": [
          "users",
          "errors"
        ]
      },
      "TimeInterval": {
        "description": "A half-open interval `[start, end)` in UTC.",
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "start",
          "end"
        ]
      },
      "ParticipantError": {
        "description": "A user whose calendars couldn't be read. The rest of the request still succeeds.",
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "connection_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "code": {
            "type": "string",
            "enum": [
              "relink_required",
              "provider_unavailable",
              "not_found",
              "no_connections"
            ],
            "description": "`relink_required`: the user must relink the connection. `provider_unavailable`: the provider failed; retry later. `not_found`: no such user or calendar. `no_connections`: the user has no connected calendars."
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "user_id",
          "connection_id",
          "code",
          "message"
        ]
      },
      "AvailabilityRequest": {
        "type": "object",
        "properties": {
          "participants": {
            "minItems": 1,
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "user_id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255,
                  "description": "Your own identifier for the end user (1–255 characters). The prefix `bentocal-test:` is reserved."
                },
                "calendar_ids": {
                  "description": "Calendar ids from any of the user's connections, `primary` (the default connection's primary calendar) or an account's email address (its primary calendar). Defaults to every active connection's primary calendar.",
                  "maxItems": 50,
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "user_id"
              ]
            }
          },
          "required": {
            "default": "all",
            "description": "`all`: every participant must be free. `any`: at least one must be; each result lists who is.",
            "type": "string",
            "enum": [
              "all",
              "any"
            ]
          },
          "periods": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "string",
                  "format": "date-time"
                },
                "end": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "required": [
                "start",
                "end"
              ]
            },
            "description": "Windows to search. Each must end after it starts, and together they span at most 62 days."
          },
          "slot_duration_minutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "multipleOf": 5,
            "description": "Length of each slot."
          },
          "slot_interval_minutes": {
            "description": "Gap between slot start times, which align to this grid from 00:00 UTC. Defaults to `slot_duration_minutes`.",
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "multipleOf": 5
          },
          "buffer_before_minutes": {
            "default": 0,
            "description": "Free time required before each slot.",
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "multipleOf": 5
          },
          "buffer_after_minutes": {
            "default": 0,
            "description": "Free time required after each slot.",
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "multipleOf": 5
          },
          "format": {
            "default": "slots",
            "description": "`slots`: bookable slots of `slot_duration_minutes` on the `slot_interval_minutes` grid. `periods`: maximal free intervals (no grid) at least `slot_duration_minutes` long; with `required: \"any\"` an interval ends wherever the set of free participants changes.",
            "type": "string",
            "enum": [
              "slots",
              "periods"
            ]
          }
        },
        "required": [
          "participants",
          "periods",
          "slot_duration_minutes"
        ]
      },
      "AvailabilityResponse": {
        "type": "object",
        "properties": {
          "slots": {
            "description": "Present when `format` is `slots`.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AvailabilitySlot"
            }
          },
          "periods": {
            "description": "Present when `format` is `periods`.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AvailabilitySlot"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantError"
            },
            "description": "Participants whose calendars could not be read. They are left out; with `required: \"all\"` that means no results."
          }
        },
        "required": [
          "errors"
        ]
      },
      "AvailabilitySlot": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "user_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Participants free for the whole interval, buffers included."
          }
        },
        "required": [
          "start",
          "end",
          "user_ids"
        ]
      },
      "UpsertEventRequest": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1024,
            "description": "Event title."
          },
          "description": {
            "type": "string",
            "maxLength": 8192
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "Must be after `start`."
          },
          "time_zone": {
            "description": "IANA time zone for the event, e.g. `Australia/Sydney`.",
            "type": "string",
            "maxLength": 64
          },
          "location": {
            "type": "string",
            "maxLength": 1024
          },
          "all_day": {
            "default": false,
            "type": "boolean"
          },
          "connection_id": {
            "description": "Write to this connection instead of the user's default connection.",
            "type": "string"
          },
          "calendar_id": {
            "description": "Write to this calendar instead of the connection's primary calendar.",
            "type": "string"
          },
          "transparency": {
            "default": "opaque",
            "description": "`opaque` blocks time in free/busy; `transparent` does not.",
            "type": "string",
            "enum": [
              "opaque",
              "transparent"
            ]
          }
        },
        "required": [
          "summary",
          "start",
          "end"
        ]
      },
      "Event": {
        "description": "The link between your event id and the provider's event.",
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Your event id from the path."
          },
          "connection_id": {
            "type": "string",
            "description": "The connection the event was written to."
          },
          "calendar_id": {
            "type": "string",
            "description": "The calendar the event was written to."
          },
          "provider_event_id": {
            "type": "string",
            "description": "The provider's id for the event."
          }
        },
        "required": [
          "event_id",
          "connection_id",
          "calendar_id",
          "provider_event_id"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "conflict",
                  "relink_required",
                  "provider_unavailable",
                  "rate_limited",
                  "company_suspended",
                  "connection_limit_reached",
                  "wrong_region",
                  "internal_error"
                ],
                "description": "Stable, machine-readable error code."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. It may change; branch on `code`."
              },
              "request_id": {
                "type": "string",
                "description": "Same as the `X-Request-Id` header. Quote it when asking for support."
              },
              "details": {
                "description": "Extra detail. Validation errors carry `issues: [{ path, message, code }]`, with `path` like `body.periods.0.end`."
              }
            },
            "required": [
              "code",
              "message",
              "request_id"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "ConnectionWebhookData": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "connection_id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "google",
              "microsoft"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "relink_required",
              "revoked"
            ],
            "description": "`active`: usable. `relink_required`: the provider stopped accepting the stored grant; send the user through a connect session with `connection_id` to fix it. `revoked`: disconnected for good."
          }
        },
        "required": [
          "user_id",
          "connection_id",
          "provider",
          "email",
          "status"
        ]
      },
      "CalendarChangedWebhookData": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "connection_id": {
            "type": "string"
          },
          "calendar_id": {
            "type": "string",
            "description": "The calendar that changed."
          },
          "provider": {
            "type": "string",
            "enum": [
              "google",
              "microsoft"
            ]
          },
          "changed_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "user_id",
          "connection_id",
          "calendar_id",
          "provider",
          "changed_at"
        ]
      },
      "ConnectionCreatedEvent": {
        "description": "A user connected a calendar account.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id (`evt_…`), also sent as `BentoCal-Event-Id`. Retries reuse it: de-duplicate on it."
          },
          "type": {
            "type": "string",
            "const": "connection.created"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "application_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/ConnectionWebhookData"
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "application_id",
          "data"
        ]
      },
      "ConnectionRelinkedEvent": {
        "description": "A user relinked a connection.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id (`evt_…`), also sent as `BentoCal-Event-Id`. Retries reuse it: de-duplicate on it."
          },
          "type": {
            "type": "string",
            "const": "connection.relinked"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "application_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/ConnectionWebhookData"
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "application_id",
          "data"
        ]
      },
      "ConnectionRelinkRequiredEvent": {
        "description": "A connection needs relinking (the provider stopped accepting its grant).",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id (`evt_…`), also sent as `BentoCal-Event-Id`. Retries reuse it: de-duplicate on it."
          },
          "type": {
            "type": "string",
            "const": "connection.relink_required"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "application_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/ConnectionWebhookData"
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "application_id",
          "data"
        ]
      },
      "ConnectionRevokedEvent": {
        "description": "A connection was revoked.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id (`evt_…`), also sent as `BentoCal-Event-Id`. Retries reuse it: de-duplicate on it."
          },
          "type": {
            "type": "string",
            "const": "connection.revoked"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "application_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/ConnectionWebhookData"
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "application_id",
          "data"
        ]
      },
      "CalendarChangedEvent": {
        "description": "Events changed on a connection's primary calendar.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id (`evt_…`), also sent as `BentoCal-Event-Id`. Retries reuse it: de-duplicate on it."
          },
          "type": {
            "type": "string",
            "const": "calendar.changed"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "application_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/CalendarChangedWebhookData"
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "application_id",
          "data"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is invalid (`invalid_request`, with `details.issues` for validation errors), or the key belongs to another region (`wrong_region`). Possible codes: `invalid_request`, `wrong_region`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing, malformed or revoked (`unauthorized`). Possible codes: `unauthorized`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The application is disabled or the provider refused the operation (`forbidden`), the account is suspended (`company_suspended`), or a new connection would pass the plan limit (`connection_limit_reached`). Possible codes: `forbidden`, `company_suspended`, `connection_limit_reached`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "forbidden",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist or belongs to another application (`not_found`). Possible codes: `not_found`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "Conflict": {
        "description": "The connection needs relinking (`relink_required`; `details.connection_id` says which), or it was revoked or the user has no default connection (`conflict`). Possible codes: `conflict`, `relink_required`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "conflict",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests (`rate_limited`). Wait for `Retry-After` seconds. Possible codes: `rate_limited`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "ProviderUnavailable": {
        "description": "Google or Microsoft failed or timed out (`provider_unavailable`). Safe to retry after `Retry-After` seconds. Possible codes: `provider_unavailable`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "provider_unavailable",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Something went wrong on our side (`internal_error`). Quote the `request_id` to support. Possible codes: `internal_error`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "…",
                "request_id": "req_8Hf2kLq9TzW1xYbN"
              }
            }
          }
        }
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Unique id of this request (`req_…`).",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key: `bc_{live|test}_{region}_…`, from the console."
      }
    }
  }
}
