{
  "openapi": "3.1.0",
  "info": {
    "title": "MorphScreen API",
    "version": "0.1.0",
    "summary": "A model's answer as UI: validated, repaired and streamed, in our format.",
    "description": "MorphScreen turns a model's answer into UI written in one format, checked by the core as it streams and repaired when it breaks a rule. Our own key goes in `X-MorphScreen-Key`. The model is included on paid plans (and 100 calls a month on the free one): leave out `Authorization` and the call runs deepseek/deepseek-v4.1-flash on our OpenRouter key (deepseek/deepseek-v4-flash when that can't serve it; ask for either), reasoning off, at most about 8,000 tokens of conversation in and 4,000 out. To run any other model, send your own OpenRouter key as `Authorization: Bearer sk-or-...`; it's passed to OpenRouter and never kept.\n\nThree ways in: `/v1/chat/completions` speaks OpenAI's and OpenRouter's chat format, so an app already on either switches by its base URL and one header; `/v1/ui` is an AG-UI run, what the MorphScreen SDKs read; `/v1/render` draws an answer you already have, with no model.\n\nThe options ride in one shape everywhere (`morphscreen` in a chat completion, `forwardedProps` in an AG-UI run, the body of a render): `packs`, `components`, `image_hosts`, `images`, `photos`, `output`, `repair`.\n\nErrors come as `{\"error\": {\"code\": <HTTP status>, \"message\", \"type\": <our reason>, \"metadata\": {\"error_type\", \"reason\"}}}`, a shape both OpenAI's and OpenRouter's SDKs read; inside a stream, a failure is an `error` on a chunk, or an AG-UI `RUN_ERROR`."
  },
  "servers": [
    {
      "url": "https://api.morphscreen.com"
    }
  ],
  "components": {
    "securitySchemes": {
      "openRouterKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your own OpenRouter key (`sk-or-...`), for any model: the model runs on it. Left out, the call runs the model included in your plan on ours. Passed to OpenRouter and nowhere else; never stored or logged."
      },
      "morphscreenKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-MorphScreen-Key",
        "description": "Your MorphScreen key (ms_live_...): the call counts against your plan. Shown once when made; only its hash is kept."
      },
      "dashboardSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "ms_session",
        "description": "The session opening a sign-up or sign-in link, or signing in with GitHub, sets."
      }
    },
    "schemas": {
      "Options": {
        "type": "object",
        "description": "How the answer is checked and what comes back.",
        "properties": {
          "packs": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "type": "string"
            },
            "description": "The core's packs to add: data, commerce, scheduling, media, docs, inputs, device, interactive, or all. Left out, every pack (\"all\"); [] for the core alone, a prompt about half as long."
          },
          "components": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "type": "object"
            },
            "description": "Your own packs, in the shape of spec/packs/*.json; 256 KB at most."
          },
          "image_hosts": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "type": "string"
            },
            "description": "Hosts the HTML's pictures may load from, besides the app's images' hosts and its photo provider's."
          },
          "images": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/AppImage"
            },
            "description": "Your own pictures, which the model shows by id."
          },
          "photos": {
            "$ref": "#/components/schemas/PhotoKey"
          },
          "output": {
            "type": "string",
            "description": "What comes back; each endpoint has its own choices."
          },
          "repair": {
            "type": "boolean",
            "default": true,
            "description": "Repair an answer with errors, and give a blank one a fresh try."
          }
        }
      },
      "Message": {
        "type": "object",
        "description": "A chat message, as OpenAI and OpenRouter take it.",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "system",
              "developer",
              "user",
              "assistant",
              "tool"
            ]
          },
          "content": {
            "type": [
              "string",
              "array",
              "null"
            ]
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": [
          "model",
          "messages"
        ],
        "description": "OpenAI's chat completion request. Every field not listed here (temperature, max_tokens, models, provider, reasoning...) goes to the model as you set it. DeepSeek V4 Flash answers with reasoning off unless you set `reasoning`.",
        "properties": {
          "model": {
            "type": "string",
            "description": "An OpenRouter model name, like deepseek/deepseek-v4.1-flash."
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Message"
            }
          },
          "stream": {
            "type": "boolean",
            "default": false
          },
          "stream_options": {
            "type": "object",
            "properties": {
              "include_usage": {
                "type": "boolean"
              }
            }
          },
          "n": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "morphscreen": {
            "$ref": "#/components/schemas/Options",
            "description": "output: text (the answer in our format, the default) or html (drawn by the core; not streamed)."
          }
        }
      },
      "Usage": {
        "type": "object",
        "description": "Tokens and cost, OpenRouter's usage object as it came (its detail fields included).",
        "required": [
          "prompt_tokens",
          "completion_tokens",
          "total_tokens"
        ],
        "properties": {
          "prompt_tokens": {
            "type": "integer"
          },
          "completion_tokens": {
            "type": "integer"
          },
          "total_tokens": {
            "type": "integer"
          },
          "cost": {
            "type": "number"
          }
        }
      },
      "Diagnostic": {
        "type": "object",
        "required": [
          "line",
          "class",
          "detail",
          "severity"
        ],
        "properties": {
          "line": {
            "type": "integer"
          },
          "class": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "note"
            ]
          }
        }
      },
      "Repair": {
        "type": "object",
        "description": "How a repair went. In a stream's last chunk it also carries the repaired `text` and whether it is `valid`.",
        "required": [
          "repaired",
          "errors_before",
          "rounds",
          "nesting",
          "ms"
        ],
        "properties": {
          "repaired": {
            "type": "boolean"
          },
          "errors_before": {
            "type": "integer"
          },
          "errors_left": {
            "type": "integer"
          },
          "rounds": {
            "type": "integer"
          },
          "nesting": {
            "type": "boolean",
            "description": "Broken nesting mended with no model."
          },
          "ms": {
            "type": "integer"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": [
                "integer",
                "array"
              ]
            },
            "description": "The lines replaced, a number or [first, last]."
          },
          "why": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "usage": {
            "type": "object",
            "properties": {
              "prompt_tokens": {
                "type": "integer"
              },
              "completion_tokens": {
                "type": "integer"
              },
              "cost": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "text": {
            "type": "string"
          },
          "valid": {
            "type": "boolean"
          }
        }
      },
      "Verdict": {
        "type": "object",
        "description": "The core's word on the answer.",
        "required": [
          "valid",
          "truncated",
          "components",
          "diagnostics"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "truncated": {
            "type": "boolean"
          },
          "components": {
            "type": "integer"
          },
          "diagnostics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Diagnostic"
            }
          },
          "repair": {
            "$ref": "#/components/schemas/Repair"
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "system_fingerprint",
          "choices",
          "morphscreen"
        ],
        "description": "Through OpenRouter, the id, model (a fallback's, when one answered) and provider are OpenRouter's own.",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "enum": [
              "chat.completion"
            ]
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "system_fingerprint": {
            "type": [
              "string",
              "null"
            ]
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "message",
                "finish_reason"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "object",
                  "required": [
                    "role",
                    "content"
                  ],
                  "properties": {
                    "role": {
                      "type": "string",
                      "enum": [
                        "assistant"
                      ]
                    },
                    "content": {
                      "type": "string"
                    },
                    "reasoning": {
                      "type": "string"
                    }
                  }
                },
                "finish_reason": {
                  "type": "string",
                  "enum": [
                    "stop",
                    "length"
                  ]
                }
              }
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          },
          "morphscreen": {
            "$ref": "#/components/schemas/Verdict"
          }
        }
      },
      "ChatCompletionChunk": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "choices"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "enum": [
              "chat.completion.chunk"
            ]
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "system_fingerprint": {
            "type": [
              "string",
              "null"
            ]
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "delta",
                "finish_reason"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "delta": {
                  "type": "object",
                  "properties": {
                    "role": {
                      "type": "string"
                    },
                    "content": {
                      "type": "string"
                    },
                    "reasoning": {
                      "type": "string"
                    }
                  }
                },
                "finish_reason": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "stop",
                    "length",
                    "error",
                    null
                  ]
                }
              }
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          },
          "morphscreen": {
            "$ref": "#/components/schemas/Verdict"
          },
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        }
      },
      "RunAgentInput": {
        "type": "object",
        "required": [
          "forwardedProps"
        ],
        "description": "AG-UI's RunAgentInput (protocol 1.0). Tools aren't supported yet: `tools` must be empty.",
        "properties": {
          "threadId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "runId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "messages": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "tools": {
            "type": "array",
            "maxItems": 0
          },
          "state": {},
          "context": {
            "type": "array"
          },
          "forwardedProps": {
            "type": "object",
            "required": [
              "model"
            ],
            "description": "The model and the options. output: agui (AG-UI events, the default) or ops (the core's op stream).",
            "properties": {
              "model": {
                "type": "string"
              },
              "packs": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "components": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "image_hosts": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "images": {
                "type": "array",
                "maxItems": 200,
                "items": {
                  "$ref": "#/components/schemas/AppImage"
                }
              },
              "photos": {
                "$ref": "#/components/schemas/PhotoKey"
              },
              "output": {
                "type": "string",
                "enum": [
                  "agui",
                  "ops"
                ]
              },
              "repair": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "AguiEvent": {
        "type": "object",
        "required": [
          "type"
        ],
        "description": "An AG-UI event: RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, then MESSAGES_SNAPSHOT when a repair put its answer in place, CUSTOM morphscreen.verdict and morphscreen.repair, and RUN_FINISHED; or RUN_ERROR.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "RUN_STARTED",
              "TEXT_MESSAGE_START",
              "TEXT_MESSAGE_CONTENT",
              "TEXT_MESSAGE_END",
              "MESSAGES_SNAPSHOT",
              "CUSTOM",
              "RUN_FINISHED",
              "RUN_ERROR"
            ]
          },
          "threadId": {
            "type": "string"
          },
          "runId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "delta": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "enum": [
              "morphscreen.verdict",
              "morphscreen.repair"
            ]
          },
          "value": {
            "type": "object"
          },
          "messages": {
            "type": "array"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      },
      "Op": {
        "type": "object",
        "required": [
          "op"
        ],
        "description": "One op of the core's op stream (spec/format.md, Output). After an `event: reset` a repaired answer's ops start again from the top; a failure is an `event: error` with {code, message}.",
        "properties": {
          "op": {
            "type": "string",
            "enum": [
              "open",
              "spans",
              "node",
              "close",
              "diag",
              "end"
            ]
          }
        }
      },
      "RenderRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "description": "An answer you already have, in our format, with the options. output: html (the default), ops or tree. Free, counted, within a fair-use limit.",
        "properties": {
          "text": {
            "type": "string"
          },
          "packs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "image_hosts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "images": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/AppImage"
            }
          },
          "photos": {
            "$ref": "#/components/schemas/PhotoKey"
          },
          "output": {
            "type": "string",
            "enum": [
              "html",
              "ops",
              "tree"
            ]
          }
        }
      },
      "RenderResponse": {
        "type": "object",
        "required": [
          "morphscreen"
        ],
        "description": "One of html, ops or tree, as output asked, with the verdict.",
        "properties": {
          "html": {
            "type": "string"
          },
          "ops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Op"
            }
          },
          "tree": {
            "type": "object"
          },
          "morphscreen": {
            "$ref": "#/components/schemas/Verdict"
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "code",
          "message",
          "type",
          "metadata"
        ],
        "properties": {
          "code": {
            "type": "integer",
            "description": "The HTTP status (502, or 504 for a model that went quiet, inside a stream)."
          },
          "message": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Our reason: invalid_json, model_required, model_not_found, openrouter_key_invalid, openrouter_out_of_credit, openrouter_rate_limited, payload_too_large, model_unavailable, model_error, model_timeout, and for sign-up: invalid_email, consent_required, bot_check_required, bot_check_failed, email_not_allowed, email_takes_no_mail, too_many_links, too_many_sign_ups, link_expired, sign_up_closed, mail_unavailable, and for billing: sign_in_required, already_subscribed, not_subscribed, billing_unavailable, billing_closed, invalid_signature"
          },
          "metadata": {
            "type": "object",
            "required": [
              "error_type",
              "reason"
            ],
            "properties": {
              "error_type": {
                "type": "string",
                "description": "OpenRouter's category for it."
              },
              "reason": {
                "type": "string"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        }
      },
      "AppImage": {
        "type": "object",
        "description": "One of your own pictures. The model shows it as ![alt](img:ID) and the API puts its URL and credit in; its host is allowed with it.",
        "required": [
          "id",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 40,
            "description": "Letters, digits, '-', '_' or '.'."
          },
          "url": {
            "type": "string",
            "description": "Where it loads from, http(s)."
          },
          "alt": {
            "type": "string",
            "maxLength": 300,
            "description": "What it shows, told to the model."
          },
          "credit": {
            "type": "string",
            "maxLength": 300,
            "description": "Shown under it, as written (Markdown links allowed)."
          }
        }
      },
      "PhotoKey": {
        "description": "The app's own photo key, for one provider: with it the model may ask for photos, searched there on the app's quota, each loading from the provider's own address with its credit. Without one (or false) the model isn't asked for photos.",
        "oneOf": [
          {
            "type": "boolean"
          },
          {
            "type": "object",
            "required": [
              "pexels"
            ],
            "properties": {
              "pexels": {
                "type": "string",
                "description": "A Pexels API key."
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "unsplash"
            ],
            "properties": {
              "unsplash": {
                "type": "object",
                "required": [
                  "key",
                  "app"
                ],
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "An Unsplash access key."
                  },
                  "app": {
                    "type": "string",
                    "description": "Your app's name on Unsplash, for the credit's links."
                  }
                }
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "openverse"
            ],
            "properties": {
              "openverse": {
                "type": "object",
                "required": [
                  "client_id",
                  "client_secret"
                ],
                "properties": {
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                }
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "SignupRequest": {
        "type": "object",
        "required": [
          "email",
          "consent",
          "turnstile"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "The address to sign up with. Throwaway domains and domains that take no mail are turned down; Gmail's dots and any +tag are the same address."
          },
          "consent": {
            "type": "string",
            "maxLength": 40,
            "description": "The version of the terms and privacy notice the person agreed to, like 2026-10-26."
          },
          "turnstile": {
            "type": "string",
            "description": "The token Cloudflare Turnstile gave the page."
          },
          "device": {
            "type": "string",
            "maxLength": 200,
            "description": "FingerprintJS's visitor id. Kept only as a hash under our own pepper: a second free account from one device runs on its own OpenRouter key only."
          },
          "from": {
            "type": "object",
            "description": "Where the sign-up came from: ref and utm_* tags, kept with the sign-up; anything else is dropped.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "GithubSigninRequest": {
        "type": "object",
        "required": [
          "consent",
          "turnstile"
        ],
        "properties": {
          "consent": {
            "type": "string",
            "maxLength": 40,
            "description": "The version of the terms and privacy notice the person agreed to, like 2026-10-26."
          },
          "turnstile": {
            "type": "string",
            "description": "The token Cloudflare Turnstile gave the page."
          },
          "device": {
            "type": "string",
            "maxLength": 200,
            "description": "FingerprintJS's visitor id. Kept only as a hash under our own pepper: a second free account from one device runs on its own OpenRouter key only."
          },
          "from": {
            "type": "object",
            "description": "Where the sign-up came from: ref and utm_* tags, kept with the sign-up; anything else is dropped.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "DeleteRequest": {
        "type": "object",
        "required": [
          "email",
          "turnstile"
        ],
        "properties": {
          "email": {
            "type": "string"
          },
          "turnstile": {
            "type": "string",
            "description": "The token Cloudflare Turnstile gave the page."
          }
        }
      },
      "Sent": {
        "type": "object",
        "required": [
          "sent"
        ],
        "properties": {
          "sent": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "The same answer whether or not the address has an account."
          }
        }
      },
      "VerifyRequest": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The 64 characters after #t= in the link."
          }
        }
      },
      "VerifyResponse": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "new": {
            "type": "boolean",
            "description": "A new account; false for a sign-in."
          },
          "key": {
            "type": "string",
            "description": "A new account's first API key (ms_live_...), shown this once and never kept."
          },
          "project": {
            "type": "string",
            "description": "The project the key belongs to."
          },
          "deleted": {
            "type": "boolean",
            "description": "A deletion link: the account and everything it held are gone."
          }
        }
      },
      "BillingStatus": {
        "type": "object",
        "required": [
          "plan",
          "yearly",
          "prices"
        ],
        "properties": {
          "plan": {
            "type": "string"
          },
          "yearly": {
            "type": "boolean"
          },
          "subscription": {
            "type": [
              "object",
              "null"
            ],
            "description": "The subscription paying now, or the newest when none does: id, plan, yearly, currency, status (created, authenticated, active, pending, halted, cancelled, completed, expired), current_end, ends_at (set when it's cancelled to end with its period), starts_at, payment_method (card, upi, emandate)."
          },
          "prices": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Each paid plan's calls and its price a month and a year, in paise (INR) and cents (USD), GST included once charged."
          },
          "gst_percent": {
            "type": "integer"
          },
          "next": {
            "type": [
              "object",
              "null"
            ],
            "description": "A subscription authorised to start later (a change, or the plan kept after a cancel, from the end of the period), with the same fields."
          },
          "given": {
            "type": "boolean",
            "description": "The plan was given by us, not paid for: it stops at its month rather than running on as extra."
          }
        }
      },
      "Subscribe": {
        "type": "object",
        "required": [
          "plan"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "build",
              "scale"
            ]
          },
          "yearly": {
            "type": "boolean"
          },
          "currency": {
            "type": "string",
            "enum": [
              "INR",
              "USD"
            ],
            "description": "Rupees for Indian billing addresses; INR if left out."
          }
        }
      },
      "ChangePlan": {
        "type": "object",
        "required": [
          "plan"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "build",
              "scale"
            ]
          },
          "yearly": {
            "type": "boolean"
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "email",
          "plan",
          "yearly",
          "created_at",
          "github",
          "limits",
          "usage",
          "days",
          "projects"
        ],
        "properties": {
          "email": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "build",
              "scale"
            ]
          },
          "yearly": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "description": "ISO 8601."
          },
          "github": {
            "type": "boolean",
            "description": "Linked to a GitHub account."
          },
          "limits": {
            "type": "object",
            "required": [
              "monthly",
              "daily",
              "renders",
              "included",
              "per_minute",
              "streams"
            ],
            "properties": {
              "monthly": {
                "type": "integer"
              },
              "daily": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "renders": {
                "type": "integer"
              },
              "included": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Calls a month on our model; null: every call."
              },
              "per_minute": {
                "type": "integer"
              },
              "streams": {
                "type": "integer"
              }
            }
          },
          "usage": {
            "type": "object",
            "required": [
              "month_calls",
              "today_calls",
              "month_renders",
              "month_included"
            ],
            "properties": {
              "month_calls": {
                "type": "integer"
              },
              "today_calls": {
                "type": "integer"
              },
              "month_renders": {
                "type": "integer"
              },
              "month_included": {
                "type": "integer"
              }
            }
          },
          "days": {
            "type": "array",
            "description": "The last 30 days (UTC) with any use.",
            "items": {
              "type": "object",
              "required": [
                "day",
                "calls",
                "renders",
                "included"
              ],
              "properties": {
                "day": {
                  "type": "string"
                },
                "calls": {
                  "type": "integer"
                },
                "renders": {
                  "type": "integer"
                },
                "included": {
                  "type": "integer"
                }
              }
            }
          },
          "projects": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "debug_logs",
                "keys"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "debug_logs": {
                  "type": "boolean"
                },
                "keys": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "prefix",
                      "created_at",
                      "last_used"
                    ],
                    "properties": {
                      "prefix": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "string"
                      },
                      "last_used": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "CallLine": {
        "type": "object",
        "required": [
          "id",
          "at",
          "project",
          "endpoint",
          "model",
          "outcome",
          "counted",
          "ms",
          "ours"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "at": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "endpoint": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "answered_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "type": "string",
            "description": "done, cut (at the model's length limit), failed, gone (the reader left), or the reason it was turned down."
          },
          "counted": {
            "type": "boolean"
          },
          "ms": {
            "type": "integer"
          },
          "first_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "valid": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "repaired": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "model_valid": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "model_errors": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object"
            }
          },
          "repair_rounds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ours": {
            "type": "boolean",
            "description": "Ran on our key."
          }
        }
      },
      "Visit": {
        "type": "object",
        "required": [
          "path"
        ],
        "properties": {
          "path": {
            "type": "string",
            "maxLength": 100,
            "description": "The page's path, as location.pathname: lower-case letters, digits and / - _ ."
          },
          "source": {
            "type": "string",
            "maxLength": 64,
            "description": "Where the visit came from: the UTM source, or the referring host; empty for none."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request can't be run as written; model_needs_your_key: a model other than the included one, asked for without your own OpenRouter key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key, or one turned down: your MorphScreen key (morphscreen_key_required, morphscreen_key_invalid) or your OpenRouter key (openrouter_key_required, openrouter_key_invalid).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "This month's calls on your plan are used (plan_quota_reached), a paid plan's month and as many again as extra (extra_limit_reached), the free plan's calls with the model included (included_calls_used, or included_calls_paused while our credit is low), or the OpenRouter account is out of credit (openrouter_out_of_credit).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The account is suspended (account_suspended), or the model's moderation refused the request (model_refused).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such model.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "The body is over 1 MB (payload_too_large), or a call with the model included has more than about 8,000 tokens of conversation (conversation_too_long).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooMany": {
        "description": "A limit reached, with Retry-After saying how long to wait: requests a minute (rate_limited), answers at once (too_many_streams), the free tier's day (daily_limit_reached), renders this month (render_limit_reached), or OpenRouter's own (openrouter_rate_limited).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadGateway": {
        "description": "The model failed or couldn't be reached, after the retries.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "The model took too long to start.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "Accounts can't be read just now; try again.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Gone": {
        "description": "link_expired: the link was used, has expired, or was never ours. Ask for a new one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "already_subscribed: the account already pays for a plan (change it instead); not_subscribed: there's no plan to change or cancel.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/chat/completions": {
      "post": {
        "operationId": "createChatCompletion",
        "summary": "A chat completion in our format, OpenAI's and OpenRouter's request and response",
        "security": [
          {
            "openRouterKey": [],
            "morphscreenKey": []
          },
          {
            "morphscreenKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer whole, or with stream true as server-sent events: chat.completion.chunk after chunk, the last with finish_reason and morphscreen (and the repaired text when a repair was needed), a usage chunk when asked, then `data: [DONE]`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "x-event-schema": {
                  "$ref": "#/components/schemas/ChatCompletionChunk"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/v1/ui": {
      "post": {
        "operationId": "runAgent",
        "summary": "An AG-UI run (protocol 1.0), what the MorphScreen SDKs read",
        "security": [
          {
            "openRouterKey": [],
            "morphscreenKey": []
          },
          {
            "morphscreenKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunAgentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Server-sent events: AG-UI events (output agui), or the core's ops (output ops).",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "x-event-schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AguiEvent"
                    },
                    {
                      "$ref": "#/components/schemas/Op"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/v1/render": {
      "post": {
        "operationId": "render",
        "summary": "An answer you already have, drawn again with no model",
        "security": [
          {
            "morphscreenKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer as HTML, ops or the tree, with the core's verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenderResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "health",
        "summary": "Whether the API is up",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "ok"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "operationId": "openapi",
        "summary": "This description",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/visit": {
      "post": {
        "operationId": "visit",
        "summary": "Counts a page seen on the website, without cookies",
        "description": "What morphscreen.com sends as a page loads. The visitor is told apart for the day by a hash of their address and user agent under a secret made each day and never stored; a page counts once per visitor a day. Bots, malformed paths and repeats are answered the same way and not counted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Visit"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Counted, or not counted: the answer is the same."
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/signup": {
      "post": {
        "operationId": "signup",
        "summary": "A magic link to sign up, or to sign in when the address has an account",
        "description": "At most 3 links an hour to one address and 10 sign-ups a day from one network. The mail goes to the address; nothing in the answer says whether it has an account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The link is on its way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/signup/verify": {
      "post": {
        "operationId": "verify",
        "summary": "Opens a magic link: a new account with its first key, a sign-in, or a deletion",
        "description": "A sign-up or a sign-in also sets the ms_session cookie (HttpOnly, Secure, SameSite=Lax, 30 days). A link works once, for 15 minutes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What the link did.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "410": {
            "$ref": "#/components/responses/Gone"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/signin/github": {
      "post": {
        "operationId": "signinGithub",
        "summary": "GitHub's page to sign up or in on",
        "description": "Takes what a sign-up by mail takes, less the address: GitHub's verified primary address is used. Sets a short-lived state cookie the way back checks; open `url` in the same browser. The account is the one linked to that GitHub account, else the one at its address (linked from then on), else a new one under sign-up's rules.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GithubSigninRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "GitHub's page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url"
                  ],
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/signin/github/callback": {
      "get": {
        "operationId": "signinGithubCallback",
        "summary": "Back from GitHub: a session, then the dashboard",
        "description": "GitHub sends the browser here. It answers with a redirect (303): to the dashboard's /welcome for a new account, its home otherwise, setting the session; or to /signin?error=<reason> (github_cancelled, github_failed, github_expired, github_email_unverified, github_other_account, email_not_allowed, too_many_sign_ups, accounts_unavailable, github_closed).",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "303": {
            "description": "To the dashboard, or back to its sign-in page with the reason.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/delete": {
      "post": {
        "operationId": "deleteAccount",
        "summary": "A link that deletes an account and everything it holds",
        "description": "Opening the link (POST /v1/signup/verify) deletes the account, its projects, keys, usage and records. The mail goes to the address; nothing in the answer says whether it has an account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The link is on its way, when the address has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "operationId": "account",
        "summary": "The dashboard's account: its plan's limits, usage by day, projects and keys",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/account/calls": {
      "get": {
        "operationId": "accountCalls",
        "summary": "The account's newest calls (100), never a prompt or an answer",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "parameters": [
          {
            "name": "failed",
            "in": "query",
            "description": "true: only calls that failed or whose answer wasn't valid.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The calls, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "calls"
                  ],
                  "properties": {
                    "calls": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CallLine"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/account/keys": {
      "post": {
        "operationId": "createKey",
        "summary": "A new key for a project, shown this once",
        "description": "At most 20 live keys a project. Only the key's hash is kept.",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "project": {
                    "type": "string",
                    "description": "The project's name (app, the first one, when left out)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "key",
                    "prefix",
                    "project"
                  ],
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "project": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/account/keys/revoke": {
      "post": {
        "operationId": "revokeKey",
        "summary": "The account's key with this prefix stops working in 8 seconds",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prefix"
                ],
                "properties": {
                  "prefix": {
                    "type": "string",
                    "maxLength": 20,
                    "description": "The key's first 12 characters, as the account lists them."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked; the key stops working at stops_at.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "revoked",
                    "stops_at"
                  ],
                  "properties": {
                    "revoked": {
                      "type": "integer"
                    },
                    "stops_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When calls with the key start being turned away."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        },
        "description": "The key keeps working for 8 seconds, so the revoke can be undone with /v1/account/keys/restore; then every call with it is turned away."
      }
    },
    "/v1/account/keys/restore": {
      "post": {
        "operationId": "restoreKey",
        "summary": "A revoke taken back while the key still works",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prefix"
                ],
                "properties": {
                  "prefix": {
                    "type": "string",
                    "maxLength": 20,
                    "description": "The key's first 12 characters, as the account lists them."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restored: the key works as before.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "restored"
                  ],
                  "properties": {
                    "restored": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Too late: the key has already stopped working (`too_late`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        },
        "description": "Within 8 seconds of a revoke the key keeps working, and this takes the revoke back. After that it has stopped for good."
      }
    },
    "/v1/account/debug": {
      "post": {
        "operationId": "setDebugLogs",
        "summary": "Keeps a project's prompts and answers 7 days, to debug, or stops",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "on"
                ],
                "properties": {
                  "project": {
                    "type": "string"
                  },
                  "on": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "project",
                    "debug_logs"
                  ],
                  "properties": {
                    "project": {
                      "type": "string"
                    },
                    "debug_logs": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/signout": {
      "post": {
        "operationId": "signout",
        "summary": "Ends this session",
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signed_out"
                  ],
                  "properties": {
                    "signed_out": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/billing": {
      "get": {
        "operationId": "billing",
        "summary": "The account's plan, its subscription and the prices",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The plan and the prices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/billing/subscribe": {
      "post": {
        "operationId": "subscribe",
        "summary": "A subscription to Build or Scale, paid in Razorpay's checkout",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "description": "Open Razorpay's checkout with `subscription` and `key`; its answer goes to /v1/billing/confirm. `pay_at` is Razorpay's own page, for when checkout can't open; it doesn't come back to the dashboard. The plan starts when Razorpay says the first payment went through.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Subscribe"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The subscription to pay.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscription": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "description": "Razorpay's public key id, for checkout."
                    },
                    "pay_at": {
                      "type": "string",
                      "description": "Razorpay's page for the payment."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/billing/confirm": {
      "post": {
        "operationId": "confirmPayment",
        "summary": "Checkout's answer for a subscription's first payment, checked",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "description": "What Razorpay's checkout hands the page when the first payment went through. The signature is checked against a subscription this account made; then Razorpay is asked where the subscription stands, and an active one starts its plan at once.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "payment",
                  "subscription",
                  "signature"
                ],
                "properties": {
                  "payment": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "razorpay_payment_id"
                  },
                  "subscription": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "razorpay_subscription_id"
                  },
                  "signature": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "razorpay_signature"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checked: where the subscription stands, and the account's plan now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "plan"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "Razorpay's status for the subscription: active once the first charge went through, authenticated while it's still on its way."
                    },
                    "plan": {
                      "type": "string"
                    },
                    "starts_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When a subscription that starts later starts charging."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/billing/change": {
      "post": {
        "operationId": "changePlan",
        "summary": "Another plan: now, or from the end of the period",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePlan"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed now, or set to change at the end of the period.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "at"
                  ],
                  "properties": {
                    "at": {
                      "type": "string",
                      "enum": [
                        "now",
                        "period_end"
                      ]
                    },
                    "plan": {
                      "type": "string"
                    },
                    "yearly": {
                      "type": "boolean"
                    },
                    "charged": {
                      "type": "integer",
                      "description": "The difference added to the next invoice, in the currency's smallest unit."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "starts_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "checkout": {
                      "type": "object",
                      "description": "subscription, key and pay_at, as /v1/billing/subscribe answers."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        },
        "description": "An upgrade on a card subscription takes effect at once, and the new plan's price for the rest of the period, less the old one's, is added to the next invoice (`charged`). Anything else (a downgrade, monthly to yearly or back, a UPI or e-mandate subscription, which Razorpay won't update) starts at the end of what's paid: open `checkout` to authorise it now; nothing is charged until `starts_at`."
      }
    },
    "/v1/billing/cancel": {
      "post": {
        "operationId": "cancelPlan",
        "summary": "Cancels the plan at the end of what's paid; then the free plan",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cancelled": {
                      "type": "boolean"
                    },
                    "until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the plan ends."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/billing/keep": {
      "post": {
        "operationId": "keepPlan",
        "summary": "The plan kept after a cancel, from the end of the period",
        "security": [
          {
            "dashboardSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Set to go on from the end of the period.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "at"
                  ],
                  "properties": {
                    "at": {
                      "type": "string",
                      "enum": [
                        "now",
                        "period_end"
                      ]
                    },
                    "plan": {
                      "type": "string"
                    },
                    "yearly": {
                      "type": "boolean"
                    },
                    "charged": {
                      "type": "integer",
                      "description": "The difference added to the next invoice, in the currency's smallest unit."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "starts_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "checkout": {
                      "type": "object",
                      "description": "subscription, key and pay_at, as /v1/billing/subscribe answers."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        },
        "description": "Razorpay can't take a cancel back, so this is a new subscription to the same plan from the end of what's paid: open `checkout` to authorise it now; nothing is charged until `starts_at`."
      }
    },
    "/v1/billing/razorpay": {
      "post": {
        "operationId": "razorpayWebhook",
        "summary": "Razorpay's webhooks",
        "description": "Checked by X-Razorpay-Signature (HMAC-SHA256 of the raw body with the webhook secret) and handled once each by X-Razorpay-Event-Id.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Handled, or handled before.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "handled": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  }
}
