{"openapi":"3.1.0","info":{"title":"getmeme API","version":"0.1.0","description":"Generates one short, guarded line of copy from a caller-supplied prompt. A null `text` is a normal outcome, never an error: always keep a static fallback."},"servers":[{"url":"/api/v1"}],"security":[{"apiKey":[]}],"paths":{"/lines":{"post":{"summary":"Create one guarded line of copy","operationId":"createLine","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}}},"responses":{"200":{"description":"Always 200 when the request was valid, even if no usable line came back.","headers":{"RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window."},"RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"RateLimit-Policy":{"schema":{"type":"string"},"description":"This key's windows, e.g. \"30;w=60, 2000;w=86400\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResponse"}}}},"400":{"description":"Invalid JSON or invalid fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Body over 16KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Content-Type is not application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. See Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"schema":{"type":"integer"}},"RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window."},"RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"RateLimit-Policy":{"schema":{"type":"string"},"description":"This key's windows, e.g. \"30;w=60, 2000;w=86400\"."}}}}}},"/health":{"get":{"summary":"Liveness and database check","security":[],"responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"Database unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/openapi.json":{"get":{"summary":"This document","security":[],"responses":{"200":{"description":"OpenAPI 3.1 JSON"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"A `gm_...` key, issued on request. Server side only: the API sends no CORS headers."}},"schemas":{"GenerateRequest":{"type":"object","additionalProperties":false,"required":["system","prompt","maxLength"],"properties":{"system":{"type":"string","minLength":1,"maxLength":2000,"description":"Role and voice rules."},"prompt":{"type":"string","minLength":1,"maxLength":4000,"description":"The ask, variables already filled in."},"maxLength":{"type":"integer","minimum":1,"maximum":500,"description":"Hard cap in characters. Longer output is rejected, not truncated."},"schema":{"type":"object","description":"JSON Schema for the model's answer. Must declare a string property `text`. Default: {text: string}. Under 4KB."},"forbid":{"type":"array","maxItems":50,"items":{"type":"string","minLength":1,"maxLength":100},"description":"Extra case-insensitive banned substrings. Never sent to the model, only enforced on its answer, so an unreleased name can be kept out of the copy without disclosing it."},"placeholders":{"type":"array","maxItems":8,"items":{"type":"string","pattern":"^[a-zA-Z][a-zA-Z0-9_]{0,30}$"},"description":"Token names, without braces, that the line may contain: [\"name\"] licenses {name}. The model is told to write them literally and you substitute afterwards, so personal data never reaches the model. Any undeclared token is rejected. Budget for the substituted length yourself: maxLength counts the token, not the value."},"timeoutMs":{"type":"integer","minimum":500,"maximum":30000,"default":30000,"description":"Total budget across every model tried."}}},"GenerateResponse":{"type":"object","required":["text","model","reason"],"properties":{"text":{"type":["string","null"],"description":"The line, with any declared placeholders left in place. Null when nothing usable came back."},"model":{"type":["string","null"],"description":"The model that actually produced `text`, which behind a router slug is the model it picked, not the slug. Null when text is null."},"reason":{"type":["string","null"],"description":"Null on success. Otherwise why there is no line: not_configured, no_usable_answer, deadline_exceeded, refused_by_model, invalid_shape, or guard:<name> naming the rule that rejected it (for example guard:max_length). The list can grow: treat an unknown value as no line."}}},"Health":{"type":"object","properties":{"ok":{"type":"boolean"},"db":{"type":"boolean"},"version":{"type":"string"}}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"requestId":{"type":"string"}}}}}}}}