{"openapi":"3.1.0","info":{"title":"GraceSoft Storehouse API","version":"1.0.1","summary":"Public-domain Proverbs and original GraceSoft quips, as static JSON.","description":"GraceSoft Storehouse is a **read-only**, versioned REST API. Every response is a static JSON\nfile served from Vercel's global CDN, so it's fast, cacheable, and needs no API key.\n\n**Envelope.** Successful responses are `{ \"data\": …, \"meta\": { \"version\", \"generatedAt\" } }`.\nLists also carry `meta.count`. Errors are `{ \"error\": { \"status\", \"code\", \"message\" }, \"meta\" }`.\n\n**Time zone.** \"Today\" is always evaluated in `Asia/Singapore` (UTC+8). New quips go live\nat 00:00 SGT on their `publishAt` date, after the daily scheduled rebuild.\n\n**Caching.** Responses carry `ETag` and `Cache-Control` headers; send `If-None-Match` to get\n`304 Not Modified`.\n\n**CORS.** Browser access is limited to an allowlist of GraceSoft origins. Server-side\nclients (bots, scripts) can call the API from anywhere.\n\n**Versioning.** Additive changes stay in `v1`. Breaking changes go to `v2`.\n\n**Licence.** Proverbs text is public domain. Quips are © D.L. (GraceSoft) — see\n`CONTENT_LICENSE.md` for terms of reuse.\n","contact":{"name":"GraceSoft","url":"https://gracesoft.dev"},"license":{"name":"MIT (code); content licensed separately","identifier":"MIT"}},"servers":[{"url":"https://api.gracesoft.dev","description":"Production"}],"security":[],"tags":[{"name":"Translations","description":"The public-domain Bible translations available."},{"name":"Proverbs","description":"The 31 chapters of the book of Proverbs, per translation."},{"name":"Quips","description":"Original GraceSoft quips and puns. Only published quips are served."}],"paths":{"/v1/translations.json":{"get":{"operationId":"listTranslations","tags":["Translations"],"summary":"List translations","description":"Every translation in the registry, sorted by code.","responses":{"200":{"$ref":"#/components/responses/TranslationList"},"304":{"$ref":"#/components/responses/NotModified"},"4XX":{"$ref":"#/components/responses/ClientError"}}}},"/v1/proverbs/{code}/index.json":{"get":{"operationId":"listChapters","tags":["Proverbs"],"summary":"List chapters","description":"The 31 chapters of Proverbs for one translation, with verse counts.","parameters":[{"$ref":"#/components/parameters/TranslationCode"}],"responses":{"200":{"$ref":"#/components/responses/ChapterList"},"304":{"$ref":"#/components/responses/NotModified"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/proverbs/{code}/{chapter}.json":{"get":{"operationId":"getChapter","tags":["Proverbs"],"summary":"Get a chapter","description":"One chapter of Proverbs, with every verse.","parameters":[{"$ref":"#/components/parameters/TranslationCode"},{"name":"chapter","in":"path","required":true,"description":"Chapter number, 1–31.","schema":{"type":"integer","minimum":1,"maximum":31}}],"responses":{"200":{"$ref":"#/components/responses/Chapter"},"304":{"$ref":"#/components/responses/NotModified"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/proverbs/{code}/today":{"get":{"operationId":"getTodaysChapter","tags":["Proverbs"],"summary":"Get today's chapter","description":"The chapter matching today's day of the month in Asia/Singapore (\"a Proverb a day\").\nOn the 19th, this is chapter 19.\n\nServed by a Vercel Function that reads the static chapter file; responses\nare cached until the next 00:00 SGT. Rate-limited.\n","x-dynamic":true,"parameters":[{"$ref":"#/components/parameters/TranslationCode"}],"responses":{"200":{"$ref":"#/components/responses/Chapter"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/quips/index.json":{"get":{"operationId":"listQuips","tags":["Quips"],"summary":"List quips","description":"Every published quip, sorted by id. Drafts and future-dated quips are never included.","responses":{"200":{"$ref":"#/components/responses/QuipList"},"304":{"$ref":"#/components/responses/NotModified"},"4XX":{"$ref":"#/components/responses/ClientError"}}}},"/v1/quips/{id}.json":{"get":{"operationId":"getQuip","tags":["Quips"],"summary":"Get a quip","description":"One published quip. Unpublished, draft, and retired ids return `404`.","parameters":[{"name":"id","in":"path","required":true,"description":"The quip's permanent id.","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"$ref":"#/components/responses/Quip"},"304":{"$ref":"#/components/responses/NotModified"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/quips/random":{"get":{"operationId":"getRandomQuip","tags":["Quips"],"summary":"Get a random quip","description":"A random published quip. Never cached. Served by a Vercel Function that reads\n`/v1/quips/index.json`. Rate-limited.\n","x-dynamic":true,"parameters":[{"name":"tag","in":"query","required":false,"description":"Only pick from quips with this tag.","schema":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"}}],"responses":{"200":{"$ref":"#/components/responses/Quip"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}},"components":{"parameters":{"TranslationCode":{"name":"code","in":"path","required":true,"description":"A translation code from `/v1/translations.json`, e.g. `kjv`.","schema":{"type":"string","pattern":"^[a-z]{2,8}$"},"examples":{"kjv":{"value":"kjv"}}}},"headers":{"ETag":{"description":"Entity tag for conditional requests.","schema":{"type":"string"}},"Cache-Control":{"description":"Caching policy for the response.","schema":{"type":"string"}}},"responses":{"TranslationList":{"description":"The translation registry.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/Cache-Control"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationListResponse"}}}},"ChapterList":{"description":"Chapter summaries for one translation.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/Cache-Control"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChapterListResponse"}}}},"Chapter":{"description":"One chapter with its verses.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/Cache-Control"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChapterResponse"}}}},"QuipList":{"description":"Every published quip.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/Cache-Control"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuipListResponse"}}}},"Quip":{"description":"One published quip.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/Cache-Control"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuipResponse"}}}},"NotModified":{"description":"The resource hasn't changed since the `If-None-Match` ETag."},"NotFound":{"description":"No resource exists at this path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ClientError":{"description":"The request can't be served, e.g. `405` for any method other than GET, HEAD, or OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"TooManyRequests":{"description":"Rate limit exceeded (60 requests a minute per IP on the dynamic endpoints). Returned by\nthe Vercel Firewall, so the body isn't the JSON error shape. Wait a minute and retry.\n"}},"schemas":{"Meta":{"type":"object","additionalProperties":false,"required":["version","generatedAt"],"properties":{"version":{"type":"string","description":"API release version (semver).","pattern":"^\\d+\\.\\d+\\.\\d+$","examples":["1.0.1"]},"generatedAt":{"type":"string","format":"date-time","description":"When this response was built."}}},"ListMeta":{"type":"object","additionalProperties":false,"required":["version","generatedAt","count"],"properties":{"version":{"$ref":"#/components/schemas/Meta/properties/version"},"generatedAt":{"$ref":"#/components/schemas/Meta/properties/generatedAt"},"count":{"type":"integer","minimum":0,"description":"Number of items in `data`."}}},"Translation":{"type":"object","additionalProperties":false,"required":["code","name","year","language","license","source"],"properties":{"code":{"type":"string","pattern":"^[a-z]{2,8}$","examples":["kjv"]},"name":{"type":"string","examples":["King James Version"]},"year":{"type":"integer","examples":[1769]},"language":{"type":"string","description":"ISO 639-1 language code.","examples":["en"]},"license":{"type":"string","const":"Public Domain"},"source":{"type":"string","format":"uri","description":"Where the text was obtained."}}},"ChapterSummary":{"type":"object","additionalProperties":false,"required":["translation","chapter","verseCount","href"],"properties":{"translation":{"type":"string","examples":["kjv"]},"chapter":{"type":"integer","minimum":1,"maximum":31},"verseCount":{"type":"integer","minimum":1},"href":{"type":"string","description":"Path to the chapter resource.","examples":["/v1/proverbs/kjv/1.json"]}}},"Verse":{"type":"object","additionalProperties":false,"required":["verse","text"],"properties":{"verse":{"type":"integer","minimum":1},"text":{"type":"string","minLength":1,"description":"Plain text (no Markdown or HTML). May contain curly quotes and em dashes."}}},"Chapter":{"type":"object","additionalProperties":false,"required":["translation","translationName","chapter","verses"],"properties":{"translation":{"type":"string","examples":["kjv"]},"translationName":{"type":"string","examples":["King James Version"]},"chapter":{"type":"integer","minimum":1,"maximum":31},"verses":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/Verse"}}}},"Quip":{"type":"object","additionalProperties":false,"required":["id","slug","variant","punchline","tags","createdAt","publishAt"],"properties":{"id":{"type":"integer","minimum":1,"description":"Permanent id; never reused."},"slug":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"variant":{"type":"string","enum":["qa","statement"],"description":"`qa` has a `question` and a `punchline`; `statement` has only a `punchline`."},"question":{"type":"string","description":"Present only when `variant` is `qa`."},"punchline":{"type":"string","description":"Plain text. Paragraphs are separated by a blank line (`\\n\\n`)."},"tags":{"type":"array","items":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"}},"createdAt":{"type":"string","description":"ISO 8601 date (`YYYY-MM-DD`) or date-time with offset."},"publishAt":{"type":"string","description":"ISO 8601 date or date-time. A date means 00:00 Asia/Singapore."}},"examples":[{"id":2,"slug":"oh-oh-oats","variant":"qa","question":"What does Santa like to have for breakfast?","punchline":"Oh, oh, oats.","tags":["food","christmas"],"createdAt":"2026-10-06","publishAt":"2026-10-06"}]},"TranslationListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Translation"}},"meta":{"$ref":"#/components/schemas/ListMeta"}}},"ChapterListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChapterSummary"}},"meta":{"$ref":"#/components/schemas/ListMeta"}}},"ChapterResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Chapter"},"meta":{"$ref":"#/components/schemas/Meta"}}},"QuipListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Quip"}},"meta":{"$ref":"#/components/schemas/ListMeta"}}},"QuipResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Quip"},"meta":{"$ref":"#/components/schemas/Meta"}}},"ErrorResponse":{"type":"object","additionalProperties":false,"required":["error","meta"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["status","code","message"],"properties":{"status":{"type":"integer","description":"The HTTP status code.","examples":[404]},"code":{"type":"string","enum":["not_found","method_not_allowed","rate_limited","internal_error"]},"message":{"type":"string","examples":["No resource exists at this path."]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}
