{
  "openapi": "3.1.0",
  "info": {
    "title": "StellarFirm public API",
    "version": "1.0.0",
    "summary": "The routes of StellarFirm that a person or an agent can call.",
    "description": "StellarFirm is a company of AI Agent Assistants that you run as the CEO. This is the small public HTTP API of the web app. Every route answers JSON, and every failure answers the same `ApiError` shape with a stable `error.code`. Authentication is described at https://stellarfirm.ai/docs/api/authentication, the routes with examples at https://stellarfirm.ai/docs/api/endpoints, and every error code at https://stellarfirm.ai/docs/api/errors.",
    "contact": {
      "name": "StellarFirm",
      "email": "hey@astrocode.tech",
      "url": "https://stellarfirm.ai/support"
    },
    "termsOfService": "https://stellarfirm.ai/terms"
  },
  "externalDocs": {
    "description": "StellarFirm API docs",
    "url": "https://stellarfirm.ai/docs/api"
  },
  "servers": [
    {
      "url": "https://stellarfirm.ai",
      "description": "StellarFirm"
    }
  ],
  "tags": [
    {
      "name": "Status",
      "description": "Is the site up, and is there a notice."
    },
    {
      "name": "Account",
      "description": "The signed-in account: access and credits."
    },
    {
      "name": "Email",
      "description": "Links that arrive in an email."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Status"
        ],
        "summary": "Check that the web app is up",
        "description": "Answers `ok: true` while the web app is running. It needs no sign-in and reads nothing from an account. Use it for an uptime check.",
        "security": [],
        "responses": {
          "200": {
            "description": "The web app is running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/site-banner": {
      "get": {
        "operationId": "getSiteBanner",
        "tags": [
          "Status"
        ],
        "summary": "Read the announcement strip",
        "description": "The notice shown at the top of the site, or `banner: null` when there is none. It is the same for everyone, needs no sign-in, and may be cached for half a minute. Browsers on other origins may read it.",
        "security": [],
        "responses": {
          "200": {
            "description": "The current notice, or null.",
            "headers": {
              "Cache-Control": {
                "description": "Shared caches may keep the answer briefly.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteBannerResponse"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/entitlement": {
      "get": {
        "operationId": "getEntitlement",
        "tags": [
          "Account"
        ],
        "summary": "Check whether the signed-in account may run",
        "description": "Answers whether the signed-in account has an active trial or licence. The desktop app asks this before it runs, and an agent acting for an account can ask it too. Only a `200` with `ok: true` means the account may run. It is never cached.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account has an active trial or licence. Ask again within `recheckAfterSeconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementAllowed"
                }
              }
            }
          },
          "401": {
            "description": "No signed-in session. `status` is `signed-out` and `error.code` is `signed_out`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementDenied"
                }
              }
            }
          },
          "403": {
            "description": "The account is signed in but may not run: the trial ended (`trial-ended`) or its record could not be read (`invalid`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementDenied"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "description": "Sign-in is not set up here (`unconfigured`) or the account could not be checked (`unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementDenied"
                }
              }
            }
          }
        }
      }
    },
    "/api/credits/balance": {
      "get": {
        "operationId": "getCreditsBalance",
        "tags": [
          "Account"
        ],
        "summary": "Read the account's credit balance",
        "description": "The signed-in account's balance as a count of StellarFirm credits. It is a number of credits and nothing else. It is never cached.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "The balance, in credits.",
            "headers": {
              "Cache-Control": {
                "description": "Private and never stored.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsBalance"
                }
              }
            }
          },
          "401": {
            "description": "No signed-in session. The `error.code` is `signed_out`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "description": "The answer could not be read right now. Try again shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/email/unsubscribe": {
      "post": {
        "operationId": "unsubscribeFromOnboardingEmails",
        "tags": [
          "Email"
        ],
        "summary": "Stop the later onboarding emails",
        "description": "One-click unsubscribe from the onboarding emails (RFC 8058). The signed `token` in the link is the credential, so no sign-in is needed. Use the link exactly as it was sent.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The signed token from the link in the email.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The later emails are stopped.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "You will not get the later onboarding notes."
              }
            }
          },
          "400": {
            "description": "The token is missing or not valid. The `error.code` is `invalid_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "502": {
            "description": "The later emails are still scheduled. The `error.code` is `upstream_failed`. Try the link again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The signed-in account's session token, sent as `Authorization: Bearer <token>`. The StellarFirm desktop app and the phone app use it. See https://stellarfirm.ai/docs/api/authentication."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__session",
        "description": "The session cookie the browser holds after you sign in at /sign-in. Sent automatically by a browser."
      }
    },
    "responses": {
      "MethodNotAllowed": {
        "description": "The route does not accept that HTTP method. The `Allow` header lists the ones it does. The `error.code` is `method_not_allowed`.",
        "headers": {
          "Allow": {
            "description": "The methods this route accepts.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "description": "Every failure from a public route has this shape. Branch on `error.code`; it does not change between releases.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiErrorDetail"
          }
        },
        "examples": [
          {
            "error": {
              "code": "not_found",
              "message": "There is no API route at /api/nothing-here.",
              "hint": "Read the route list at /openapi.json, or start at /docs/api.",
              "docs": "https://stellarfirm.ai/docs/api/errors"
            }
          }
        ]
      },
      "ApiErrorDetail": {
        "type": "object",
        "required": [
          "code",
          "message",
          "hint",
          "docs"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "A stable machine-readable code.",
            "enum": [
              "not_found",
              "method_not_allowed",
              "signed_out",
              "trial_ended",
              "forbidden",
              "invalid_request",
              "invalid_token",
              "not_configured",
              "balance_unavailable",
              "credits_not_configured",
              "billing_not_configured",
              "paid_only",
              "checkout_failed",
              "portal_failed",
              "limit_required",
              "limit_failed",
              "invalid_signature",
              "fulfilment_failed",
              "unavailable",
              "upstream_failed",
              "too_large",
              "internal_error"
            ],
            "examples": [
              "signed_out"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong, in one plain sentence."
          },
          "hint": {
            "type": "string",
            "description": "What to do next."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Where the error codes are explained.",
            "examples": [
              "https://stellarfirm.ai/docs/api/errors"
            ]
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "service",
          "surface",
          "agent"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "service": {
            "type": "string",
            "description": "A fixed identifier for the web service."
          },
          "surface": {
            "type": "string",
            "description": "Which part of the product answered."
          },
          "agent": {
            "type": "string",
            "description": "Whether assistants run in this service."
          }
        }
      },
      "SiteBanner": {
        "type": "object",
        "required": [
          "kind",
          "message"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "promo",
              "warning"
            ],
            "description": "A `promo` shows on the public site only. A `warning` shows everywhere."
          },
          "message": {
            "type": "string",
            "description": "The notice text."
          }
        }
      },
      "SiteBannerResponse": {
        "type": "object",
        "required": [
          "banner"
        ],
        "properties": {
          "banner": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SiteBanner"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "EntitlementAllowed": {
        "type": "object",
        "required": [
          "ok",
          "status",
          "recheckAfterSeconds"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "status": {
            "type": "string",
            "enum": [
              "licensed",
              "trial"
            ]
          },
          "trialEndsAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the trial ends. Present on a trial."
          },
          "assistantLimit": {
            "type": "integer",
            "minimum": 0,
            "description": "How many named assistants the account may hire."
          },
          "recheckAfterSeconds": {
            "type": "integer",
            "minimum": 1,
            "description": "Ask again within this many seconds."
          }
        }
      },
      "EntitlementDenied": {
        "type": "object",
        "description": "The account may not run. It carries the `ApiError` fields too, so a client can branch on `error.code`.",
        "required": [
          "ok",
          "status",
          "message",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "status": {
            "type": "string",
            "enum": [
              "signed-out",
              "trial-ended",
              "invalid",
              "unconfigured",
              "unavailable"
            ]
          },
          "message": {
            "type": "string"
          },
          "error": {
            "$ref": "#/components/schemas/ApiErrorDetail"
          }
        }
      },
      "CreditsBalance": {
        "type": "object",
        "required": [
          "credits"
        ],
        "properties": {
          "credits": {
            "type": "number",
            "minimum": 0,
            "description": "The balance, as a count of credits."
          }
        }
      }
    }
  }
}
