{
  "openapi": "3.1.0",
  "info": {
    "title": "Saasie Batteries",
    "description": "Analytics, mail and inference for a running Saasie. Authenticated by the service token the platform injects into its containers — never by a session, which belongs to a person.",
    "version": "0.1.0"
  },
  "components": {
    "securitySchemes": {
      "saasieToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Saasie's service token, injected as `{ battery: <name>, property: token }`."
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "meta"
        ],
        "summary": "Liveness probe",
        "responses": {
          "200": {
            "description": "The service is up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/version": {
      "get": {
        "operationId": "getVersion",
        "tags": [
          "meta"
        ],
        "summary": "What is deployed",
        "responses": {
          "200": {
            "description": "Package name and version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "version"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/types": {
      "post": {
        "operationId": "postV1AnalyticsTypes",
        "tags": [
          "analytics"
        ],
        "summary": "Register an event type",
        "description": "Idempotent, so an app can call it at boot without checking. Registering an existing type replaces the properties it declares — which is how a type gains one, and also how an event carrying an old property starts being refused.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9_.-]{0,62}$",
                    "description": "The event type, e.g. `signup`. Registering it again updates it."
                  },
                  "properties": {
                    "description": "The properties every event of this type may carry, and what each one means. An event carrying anything else is refused.",
                    "default": {},
                    "type": "object",
                    "propertyNames": {
                      "type": "string",
                      "pattern": "^[a-z][a-z0-9_.-]{0,62}$"
                    },
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "string",
                        "number",
                        "boolean"
                      ]
                    }
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The type, as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "properties": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "createdAt": {
                      "type": "number"
                    },
                    "updatedAt": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "name",
                    "properties",
                    "createdAt",
                    "updatedAt"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The name or a property name is not usable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The Saasie is stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getV1AnalyticsTypes",
        "tags": [
          "analytics"
        ],
        "summary": "What this Saasie has registered",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Every registered type",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "types": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "properties": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            }
                          },
                          "createdAt": {
                            "type": "number"
                          },
                          "updatedAt": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "name",
                          "properties",
                          "createdAt",
                          "updatedAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "types"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/events": {
      "post": {
        "operationId": "postV1AnalyticsEvents",
        "tags": [
          "analytics"
        ],
        "summary": "Log events",
        "description": "Every event's type must be registered, and every property must be one that type declared. Both are 400s rather than silently-stored data, because an event nobody meant to send is worse than one that was refused.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "events": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "pattern": "^[a-z][a-z0-9_.-]{0,62}$"
                        },
                        "at": {
                          "description": "When it happened — epoch milliseconds or an ISO timestamp. Left out, it is now.",
                          "anyOf": [
                            {
                              "type": "integer",
                              "minimum": -9007199254740991,
                              "maximum": 9007199254740991
                            },
                            {
                              "type": "string",
                              "format": "date-time",
                              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                            }
                          ]
                        },
                        "properties": {
                          "description": "Must be properties this type declared. Values are stored as strings.",
                          "default": {},
                          "type": "object",
                          "propertyNames": {
                            "type": "string",
                            "pattern": "^[a-z][a-z0-9_.-]{0,62}$"
                          },
                          "additionalProperties": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "number"
                              },
                              {
                                "type": "boolean"
                              }
                            ]
                          }
                        }
                      },
                      "required": [
                        "type"
                      ]
                    }
                  }
                },
                "required": [
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many were stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recorded": {
                      "type": "number",
                      "description": "How many events were stored."
                    },
                    "remainingToday": {
                      "type": "number",
                      "description": "How many more this Saasie may log before the daily cap."
                    }
                  },
                  "required": [
                    "recorded",
                    "remainingToday"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "An unregistered type, or an undeclared property",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The Saasie is stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "The daily event cap is spent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "501": {
            "description": "This Saasie has no analytics storage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The analytics cluster is unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/query": {
      "post": {
        "operationId": "postV1AnalyticsQuery",
        "tags": [
          "analytics"
        ],
        "summary": "Count events over a range",
        "description": "A constrained query, never SQL: a type, a range, a bucket size, and optionally one declared property to split by. That is enough to draw a chart and small enough that nothing a caller sends becomes part of a statement.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9_.-]{0,62}$"
                  },
                  "since": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      {
                        "type": "string",
                        "format": "date-time",
                        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                      }
                    ],
                    "description": "Start of the range, inclusive. Epoch milliseconds or ISO."
                  },
                  "until": {
                    "description": "End of the range, exclusive. Left out, it is now.",
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      {
                        "type": "string",
                        "format": "date-time",
                        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                      }
                    ]
                  },
                  "interval": {
                    "default": "day",
                    "type": "string",
                    "enum": [
                      "hour",
                      "day",
                      "week",
                      "month"
                    ]
                  },
                  "groupBy": {
                    "description": "A property of this type to split the counts by. It must be one the type declared.",
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9_.-]{0,62}$"
                  }
                },
                "required": [
                  "type",
                  "since"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Counts per bucket",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "interval": {
                      "type": "string",
                      "enum": [
                        "hour",
                        "day",
                        "week",
                        "month"
                      ]
                    },
                    "since": {
                      "type": "number"
                    },
                    "until": {
                      "type": "number"
                    },
                    "groupBy": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "buckets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "number",
                            "description": "Epoch ms at the start of the bucket."
                          },
                          "group": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "count": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "at",
                          "group",
                          "count"
                        ]
                      }
                    },
                    "total": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "type",
                    "interval",
                    "since",
                    "until",
                    "groupBy",
                    "buckets",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Unknown type, undeclared groupBy, or too wide a range",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "501": {
            "description": "This Saasie has no analytics storage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The analytics cluster is unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/mail/send": {
      "post": {
        "operationId": "postV1MailSend",
        "tags": [
          "mail"
        ],
        "summary": "Send an email",
        "description": "From this Saasie's own address, to anybody, capped per day. The address is assigned by the platform — a tenant choosing its own local part is a tenant choosing `security@`.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string",
                    "format": "email",
                    "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  },
                  "subject": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100000
                  }
                },
                "required": [
                  "to",
                  "subject",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "It was accepted by the provider",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "type": "boolean",
                      "const": true
                    },
                    "from": {
                      "type": "string"
                    },
                    "remainingToday": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "sent",
                    "from",
                    "remainingToday"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A malformed address, or an empty subject or body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The Saasie is stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "The daily cap is spent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "501": {
            "description": "This Saasie has no address yet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The mail provider refused it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/inference/models": {
      "get": {
        "operationId": "getV1InferenceModels",
        "tags": [
          "inference"
        ],
        "summary": "What this Saasie can call, and what is left today",
        "description": "The names are logical and stable. Which model actually serves one is the platform's to change — that is what makes it possible to improve them without breaking your app.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "generate",
                              "embed"
                            ]
                          },
                          "tokensPerDay": {
                            "type": "number"
                          },
                          "remainingToday": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "name",
                          "kind",
                          "tokensPerDay",
                          "remainingToday"
                        ]
                      }
                    }
                  },
                  "required": [
                    "models"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/inference/generate": {
      "post": {
        "operationId": "postV1InferenceGenerate",
        "tags": [
          "inference"
        ],
        "summary": "Generate text",
        "description": "One prompt or a conversation, one answer. Not streamed: the answer arrives whole, which keeps the daily cap checkable before the work is done rather than after.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "description": "Which logical model to use. Only `saasie/fast` generates.",
                    "default": "saasie/fast",
                    "type": "string",
                    "enum": [
                      "saasie/fast",
                      "saasie/embed"
                    ]
                  },
                  "prompt": {
                    "description": "A single instruction. Shorthand for one `user` message.",
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100000
                  },
                  "messages": {
                    "description": "A conversation, oldest first. Use instead of `prompt`.",
                    "minItems": 1,
                    "maxItems": 64,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "system",
                            "user",
                            "assistant"
                          ]
                        },
                        "content": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 100000
                        }
                      },
                      "required": [
                        "role",
                        "content"
                      ]
                    }
                  },
                  "maxTokens": {
                    "description": "Ceiling on the answer's length. Capped by the platform; left out, the platform's default.",
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 9007199254740991
                  },
                  "temperature": {
                    "default": 0.7,
                    "type": "number",
                    "minimum": 0,
                    "maximum": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The completion and what it cost",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "model": {
                      "type": "string",
                      "description": "The logical model that served it."
                    },
                    "finishReason": {
                      "type": "string",
                      "enum": [
                        "stop",
                        "length",
                        "other"
                      ],
                      "description": "`length` means the answer hit `maxTokens` and is cut off — ask again with a larger one."
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "inputTokens": {
                          "type": "number"
                        },
                        "outputTokens": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "inputTokens",
                        "outputTokens"
                      ]
                    },
                    "remainingToday": {
                      "type": "number",
                      "description": "Generation tokens left before the daily cap."
                    }
                  },
                  "required": [
                    "text",
                    "model",
                    "finishReason",
                    "usage",
                    "remainingToday"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Neither or both of prompt and messages, or an embedding model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The Saasie is stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "The daily token cap is spent, or this request would not fit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The inference provider failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/inference/embed": {
      "post": {
        "operationId": "postV1InferenceEmbed",
        "tags": [
          "inference"
        ],
        "summary": "Embed text as vectors",
        "description": "One string or a batch. Vectors come back in the order they were sent, which is what makes a batch usable — pair them with your own ids by position.",
        "security": [
          {
            "saasieToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "description": "Which logical model to use. Only `saasie/embed` embeds.",
                    "default": "saasie/embed",
                    "type": "string",
                    "enum": [
                      "saasie/fast",
                      "saasie/embed"
                    ]
                  },
                  "input": {
                    "anyOf": [
                      {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100000
                      },
                      {
                        "minItems": 1,
                        "type": "array",
                        "items": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 100000
                        }
                      }
                    ],
                    "description": "One string or many. Many is not just convenience — a batch is one round trip and one cold start."
                  }
                },
                "required": [
                  "input"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One vector per input",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "embeddings": {
                      "type": "array",
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "number"
                        }
                      },
                      "description": "One vector per input, in the order they were sent."
                    },
                    "model": {
                      "type": "string"
                    },
                    "dimensions": {
                      "type": "number",
                      "description": "Length of each vector. Read it rather than hard-coding it: it changes if the model behind `saasie/embed` does."
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "tokens": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "tokens"
                      ]
                    },
                    "remainingToday": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "embeddings",
                    "model",
                    "dimensions",
                    "usage",
                    "remainingToday"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Too large a batch, or a generation model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid service token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The Saasie is stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "The daily embedding cap is spent, or this batch would not fit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The inference provider failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}
