openapi: 3.0.0 info: title: 'Stories API — Islamic Network' description: 'The marifa.org stories corpus: teaching stories from the tradition, with artwork, shared-taxonomy tags and people from the shared registry. Every response is wrapped in the `{code, status, data}` envelope shared across Islamic Network APIs; list endpoints add a `meta` sibling. Language keys are BCP 47 tags; empty values are omitted entirely. Lists summarize (no body), the item endpoint expands.' contact: url: 'https://islamic.network' email: hello@islamic.network version: 1.0.0 servers: - url: 'https://stories.api.islamic.network/v1' description: Production paths: '/assets/{id}': get: tags: - assets summary: 'Story artwork, streamed from CMS storage' operationId: getAsset parameters: - name: id in: path description: 'The asset uuid, as carried in `image.id`.' required: true schema: type: string format: uuid responses: '200': description: 'The image bytes, aggressively cacheable (ids are immutable).' '404': $ref: '#/components/responses/Error' '/documentation/openapi/{format}': get: tags: - documentation summary: 'This specification, as OpenAPI 3' operationId: getOpenApiSpec parameters: - name: format in: path required: true schema: type: string enum: - yaml - json responses: '200': description: 'The OpenAPI document.' /search: get: tags: - search summary: 'Diacritic-folded search across stories' description: 'Matching is diacritic-folded (Ṣ→s, ā→a, ʿ/ʾ dropped, Arabic tashkīl stripped) across every language key of titles, descriptions, bodies, sources and tags — `rumi`, `Rūmī` and `الرُّومِي` all match. Multiple query words AND together.' operationId: search parameters: - name: q in: query description: 'The query; at least 2 characters.' required: true schema: type: string example: forgotten minLength: 2 - $ref: '#/components/parameters/page' - $ref: '#/components/parameters/limit' responses: '200': description: 'Matching stories as references with the field paths that matched.' content: application/json: schema: allOf: - { $ref: '#/components/schemas/Envelope' } - { properties: { meta: { $ref: '#/components/schemas/Meta' }, data: { properties: { query: { type: string }, total: { type: integer }, results: { type: array, items: { $ref: '#/components/schemas/SearchResult' } } }, type: object } }, type: object } '400': $ref: '#/components/responses/Error' /stories: get: tags: - stories summary: 'All stories (summaries, no body), newest first' operationId: listStories parameters: - name: tag in: query description: 'Only stories carrying this topic tag.' schema: type: string example: remembrance - name: person in: query description: 'Only stories about this person (registry slug).' schema: type: string example: jalal-ad-din-muhammad-rumi - $ref: '#/components/parameters/page' - $ref: '#/components/parameters/limit' responses: '200': description: 'Paginated story summaries.' content: application/json: schema: allOf: - { $ref: '#/components/schemas/Envelope' } - { properties: { meta: { $ref: '#/components/schemas/Meta' }, data: { type: array, items: { $ref: '#/components/schemas/StorySummary' } } }, type: object } '/stories/{slug}': get: tags: - stories summary: 'Full story with its body' operationId: getStory parameters: - $ref: '#/components/parameters/slug' responses: '200': description: 'The story, expanded.' content: application/json: schema: allOf: - { $ref: '#/components/schemas/Envelope' } - { properties: { data: { $ref: '#/components/schemas/StoryDetail' } }, type: object } '404': $ref: '#/components/responses/Error' /tags: get: tags: - tags summary: 'The tag index, with counts' description: 'The topic slugs used by stories, with their shared-taxonomy names — the same tags collection serves the quotes API, so a topic can be browsed across domains.' operationId: listTags parameters: - $ref: '#/components/parameters/page' - $ref: '#/components/parameters/limit' responses: '200': description: 'Paginated tags, slug order.' content: application/json: schema: allOf: - { $ref: '#/components/schemas/Envelope' } - { properties: { meta: { $ref: '#/components/schemas/Meta' }, data: { type: array, items: { $ref: '#/components/schemas/Tag' } } }, type: object } components: schemas: Name: description: 'Language-keyed text. Keys are BCP 47 tags; `ar-Latn` is the transliteration. Only non-empty languages appear (`{ "en": … }` today; translations appear additively).' type: object example: en: 'One Thing Which Must Never Be Forgotten' additionalProperties: type: string Prose: description: 'Language-keyed prose. `raw` is the authored Markdown; `html` is rendered from it (Arabic-script runs wrapped in ``).' type: object additionalProperties: required: - raw - html properties: raw: description: "OpenAPI document root and shared components. Endpoint annotations live on\nthe v1 controllers; the assembled spec is served by\nGET /v1/documentation/openapi/{yaml|json}." type: string html: type: string type: object Image: description: "The story's artwork, a CMS asset. The url is this API's gateway-cached /assets passthrough; size variants can be added later via query params without breaking." required: - id - url properties: id: type: string format: uuid url: type: string type: object PersonRef: description: 'A person the story is about. The slug is the ecosystem-wide identifier owned by the People API; `url` points into that registry. People are never tags.' required: - slug - name - url properties: slug: type: string example: jalal-ad-din-muhammad-rumi name: $ref: '#/components/schemas/Name' url: type: string type: object Meta: description: 'Pagination block, sibling of `data` on list endpoints.' required: - total - page - limit - pages properties: total: type: integer page: type: integer limit: type: integer pages: type: integer type: object Envelope: required: - code - status properties: code: type: integer example: 200 status: type: string example: OK type: object StorySummary: required: - slug - url - published - title properties: slug: description: 'From the marifa.org post URL (permanent).' type: string url: type: string published: type: string format: date example: '2025-05-21' title: $ref: '#/components/schemas/Name' description: $ref: '#/components/schemas/Name' image: $ref: '#/components/schemas/Image' tags: description: 'Shared-taxonomy topic slugs.' type: array items: type: string source: description: 'The citation exactly as authored.' type: string example: 'Discourses of Rumi ق' people: type: array items: $ref: '#/components/schemas/PersonRef' type: object StoryDetail: allOf: - $ref: '#/components/schemas/StorySummary' - properties: body: $ref: '#/components/schemas/Prose' type: object Tag: required: - slug - name - stories - url properties: slug: type: string example: remembrance name: type: string example: Remembrance stories: description: 'How many stories carry the tag.' type: integer url: description: "This API's filtered story list." type: string type: object SearchResult: required: - type - matched_in - slug - title - published - url properties: type: type: string enum: - story matched_in: description: 'Language-keyed field paths that matched, e.g. `body.en`.' type: array items: type: string slug: type: string title: $ref: '#/components/schemas/Name' published: type: string format: date url: type: string type: object responses: Error: description: 'Error — the envelope with a message string as `data`.' content: application/json: schema: required: - code - status - data properties: code: type: integer example: 404 status: type: string example: 'NOT FOUND' data: type: string example: 'Story not found.' type: object parameters: page: name: page in: query description: 'Page number (default 1).' schema: type: integer default: 1 minimum: 1 limit: name: limit in: query description: 'Records per page (default 50, max 200).' schema: type: integer default: 50 maximum: 200 minimum: 1 slug: name: slug in: path description: "The record's canonical URL key." required: true schema: type: string tags: - name: stories description: 'The stories themselves' - name: tags description: 'The shared topic taxonomy' - name: assets description: 'Story artwork passthrough' - name: search description: 'Diacritic-folded search' - name: documentation description: 'This specification'