# getmeme > getmeme is an HTTP API that writes one short line of copy (a greeting, a nudge, an empty-state line), checks it against eight fixed guards, and returns it. When no line passes, it returns HTTP 200 with `text: null` and a machine-readable `reason`, so a caller can always fall back to a static line. Status: beta. Keys are issued by hand. Base URL: https://getmeme.warmhop.com/api/v1 Machine-readable spec: https://getmeme.warmhop.com/api/v1/openapi.json (OpenAPI 3.1, generated by the running service, so it cannot drift from it) Contact and API keys: getmeme@warmhop.com ## Docs - [Documentation](https://getmeme.warmhop.com/docs): the full human reference, with a keyless demo - [OpenAPI spec](https://getmeme.warmhop.com/api/v1/openapi.json): request and response schemas for every public route - [API catalog](https://getmeme.warmhop.com/.well-known/api-catalog): RFC 9727 linkset pointing at the spec, docs and health check - [Health](https://getmeme.warmhop.com/api/v1/health): `{ "ok": true, "db": true, "version": "..." }`, or 503 when the database is down ## Authentication Every call to `/api/v1/lines` carries `Authorization: Bearer gm_...`. A missing, unknown, revoked or expired key is `401 invalid_api_key` (the four cases are not told apart). There is no sign-up page: write to getmeme@warmhop.com with what you are building and roughly how often you will call. A key is shown once and only its hash is stored. Keep it on a server: the API sends no CORS headers, so browser calls fail by design. ## Create a line: POST /api/v1/lines Request body (JSON, `Content-Type: application/json` required, max 16KB, unknown fields rejected): - `system` (required, string, 1 to 2000): the voice. Who is writing, in what tone, about what product. - `prompt` (required, string, 1 to 4000): the ask, with your own variables already filled in. - `maxLength` (required, integer, 1 to 500): hard cap in characters. A longer line is rejected, never trimmed. - `placeholders` (string[], up to 8): token names without braces. `["name"]` lets `{name}` appear in the line; you substitute the value yourself. Any undeclared token is rejected. - `forbid` (string[], up to 50, each 1 to 100 chars): banned substrings, case insensitive, after Unicode normalisation. Never sent to the model and never stored. - `schema` (object): JSON Schema for the answer; must declare a string property `text`. Only `text` is returned. - `timeoutMs` (integer, 500 to 30000, default 30000): budget shared across every model tried. Set your own socket timeout above it. Example: ``` curl -sS https://getmeme.warmhop.com/api/v1/lines \ -H "Authorization: Bearer $GETMEME_API_KEY" \ -H "Content-Type: application/json" \ -d '{"system":"You write one friendly greeting line for a link tracking app.","prompt":"Greet them on a Monday morning. Address them as {name}, exactly once.","maxLength":80,"placeholders":["name"],"forbid":["acme"]}' ``` Success: `{ "text": "Happy Monday, {name}. The coffee already forgave you.", "model": "qwen/qwen3.8-27b:free", "reason": null }` No line (still HTTP 200): `{ "text": null, "model": null, "reason": "guard:max_length" }` `model` is the model that actually answered. The same request does not return the same line twice, and there is no idempotency key: a retry is a new call and a new rate-limit slot. Every response carries `Cache-Control: no-store`. ## The eight guards Checked in this order; the first rejection wins, nothing is repaired, and the rejecting guard is named in `reason` as `guard:`. They check shape, not meaning: treat output as model text you are responsible for. 1. `normalise`: NFKC, trim, strip wrapping quotes; empty after that is a rejection 2. `single_line`: any line break 3. `no_markdown`: backticks, asterisks, tildes, bullets, headings, link syntax 4. `max_length`: longer than `maxLength`, counted in characters 5. `no_em_dash`: an em dash 6. `placeholders`: square or angle brackets, undeclared `{token}`, other template syntax 7. `no_urls_or_emails`: a link, a bare domain on a common suffix, or an `@` between two non-spaces 8. `forbid`: any substring from your `forbid` list ## Values of `reason` when `text` is null - `not_configured`: the service has no model or provider token configured - `no_usable_answer`: every model was tried and none gave a usable answer (rate limited, erroring, unreachable, or not JSON) - `deadline_exceeded`: `timeoutMs` ran out - `refused_by_model`: no answer, and a model or its provider refused on content grounds (a content filter or a moderation response); rephrase rather than retry - `invalid_shape`: valid JSON came back with no string `text` - `guard:`: a line came back and one guard rejected it - `blocked_topic`: keyless demo only; the request or answer touched a topic the demo does not take (news events, politics, war, religion, health, money and similar). Keyed calls are not under this list. Treat an unrecognised `reason` as "no line": the list can grow. Recommended client pattern: render a static line immediately, call getmeme, swap in the generated line only if `text` is a string. ## Errors Shape: `{ "error": { "code": "...", "message": "...", "requestId": "..." } }`. `requestId` matches the `X-Request-Id` header. - 400 `invalid_json`, 400 `invalid_request`, 401 `invalid_api_key`, 404 `not_found`, 413 `payload_too_large`, 415 `unsupported_media_type`, 429 `rate_limited` (with `Retry-After` in seconds), 500 `internal_error` ## Rate limits Per key, per minute and per day. A new key starts at 15 a minute and 500 a day. `/lines` responses carry `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy` (for example `15;w=60, 500;w=86400`). A 400 or 415 still costs a request, because the limiter runs before validation. ## Privacy Prompts go to OpenRouter and on to whichever model answers. Do not put personal data in `prompt`: declare a placeholder such as `{name}` and substitute on your side. Each call (system, prompt, raw model answers, returned line) is kept 14 days for debugging, readable only by getmeme; `forbid` terms are never stored. Zero retention for a key is available on request. ## Optional - [Landing page](https://getmeme.warmhop.com/): a live example. Its greeting is generated with a `{place}` token and the visitor's country is substituted afterwards, so the model never sees it. - [Security contact](https://getmeme.warmhop.com/.well-known/security.txt)