# gray.gift — Full Documentation > Interactive fiction platform for branching, choose-your-own-adventure stories. ## Overview gray.gift lets authors create stories composed of **segments** (passages of text) connected by **choices** (links between segments). Readers navigate the story by making choices, reaching different endings based on their path. Stories can optionally include RPG mechanics: stats, items, and skill checks. ## Concepts ### Stories A story has a title, slug, description, cover image, author, and a start segment. Stories can be draft, published, or archived. Only published stories are visible to readers and the API. ### Mystery Mode Stories have a `mystery_mode` flag (default: on). When enabled, the story must be played sequentially via server-tracked sessions. The `segment`, `choice`, and `tree` endpoints return a `403 mystery_mode` error for these stories. Instead, use `start` to begin a session, then `move` to make choices. The server tracks your position, enforces RPG requirements, and applies effects automatically. Sessions expire after 24 hours. ### Segments A segment is a unit of story content. It has a body (Markdown), an optional image, and can be marked as an ending. Segments connect to other segments via choices. ### Choices A choice is a labeled link from one segment to another. Choices can have requirements (minimum stat value or possession of an item) that gate access in RPG stories. ### RPG Mechanics (optional per story) - **Stats**: Named numeric values (e.g., "Courage", "Health") with min/max bounds and defaults. Segments can apply stat changes on arrival. - **Items**: Named objects that can be granted or removed by segment effects. Choices can require possession of an item. - **Segment Effects**: Applied when a reader arrives at a segment. Types: `stat_change`, `item_grant`, `item_remove`. ### Tags Stories can be tagged for browsing/filtering. Tags have names and URL-friendly slugs. ## Response format note All responses are JSON. Segment content is returned in two fields: - `body` — raw Markdown source. **Use this for LLM consumption** (smaller, cleaner). - `body_html` — rendered HTML. Only useful if you need to display formatted output. --- ## Reader API Reference Base URL: `https://gray.gift/api/reader.php` All endpoints accept GET requests. No authentication required. Responses are JSON with `Content-Type: application/json`. CORS is enabled. --- ### GET ?action=stories List or search published stories. **Parameters:** | Param | Type | Default | Description | |--------|--------|----------|-------------| | q | string | — | Search title and description | | tag | string | — | Filter by tag slug | | sort | string | newest | Sort order: `newest`, `top`, `most-read` | | page | int | 1 | Page number | | limit | int | 20 | Results per page (max 50) | **Response:** ```json { "stories": [ { "id": 1, "title": "The Dark Forest", "slug": "the-dark-forest", "description": "A mysterious journey...", "cover_image_url": "https://...", "author": "jsmith", "has_rpg_mechanics": false, "mystery_mode": true, "segment_count": 12, "avg_rating": 4.5, "rating_count": 8, "completions": 23, "created_at": "2026-01-15 10:30:00" } ], "total": 42, "page": 1, "limit": 20, "total_pages": 3 } ``` --- ### GET ?action=story&slug=SLUG Get full details for a single story. **Parameters:** | Param | Type | Required | Description | |-------|--------|----------|-------------| | slug | string | yes | Story URL slug | **Response:** ```json { "id": 1, "title": "The Dark Forest", "slug": "the-dark-forest", "description": "A mysterious journey...", "cover_image_url": "https://...", "author": "jsmith", "has_rpg_mechanics": true, "mystery_mode": false, "start_segment_id": 5, "segment_count": 12, "avg_rating": 4.5, "rating_count": 8, "tags": [ {"name": "Fantasy", "slug": "fantasy"}, {"name": "Horror", "slug": "horror"} ], "rpg": { "stats": [ {"name": "courage", "default_value": 5, "min_value": 0, "max_value": 10} ], "items": [ {"id": 3, "name": "Rusty Key", "description": "Opens something..."} ] }, "created_at": "2026-01-15 10:30:00" } ``` The `rpg` field is `null` for non-RPG stories. `start_segment_id` is only included for non-mystery stories. For mystery mode stories, use `action=start` to begin a session. --- ### GET ?action=start&slug=SLUG Start a mystery mode play session. Returns a token and the first segment. Works for all stories (mystery mode or not). **Parameters:** | Param | Type | Required | Description | |-------|--------|----------|-------------| | slug | string | yes | Story URL slug | **Response:** ```json { "token": "a1b2c3d4...64-char-hex-token", "segment": { "id": 5, "body": "You stand at the edge of a dark forest...", "body_html": "
You stand at the edge of a dark forest...
", "is_ending": false, "image_url": null, "image_alt": null, "segment_number": 1, "total_segments": 12 }, "story": { "slug": "the-dark-forest", "title": "The Dark Forest" }, "choices": [ { "id": 10, "label": "Enter the forest", "available": true }, { "id": 11, "label": "Turn back", "requires_stat": "courage", "requires_value": 3, "available": false, "reason": "Requires courage >= 3 (you have 1)" } ], "rpg_state": { "stats": {"courage": 5, "health": 10}, "inventory": [] }, "session": { "segments_visited": 1, "is_completed": false, "expires_at": "2026-03-15 14:30:00" } } ``` Notes: - Save the `token` — you need it for `move` and `state` calls. - Choices do **not** include `to_segment_id` — no peeking ahead. - Each choice has an `available` boolean. Unavailable choices include a `reason`. - `rpg_state` is only present for RPG stories. - Sessions expire after 24 hours. --- ### GET ?action=move&token=TOKEN&choice_id=ID Make a choice in a mystery mode session. Advances the session to the next segment, applies RPG effects, and returns the new state. **Parameters:** | Param | Type | Required | Description | |-----------|--------|----------|-------------| | token | string | yes | Session token from `start` | | choice_id | int | yes | Choice ID from the current segment's choices | **Response:** ```json { "segment": { "id": 6, "body": "The trees close in around you...", "body_html": "The trees close in around you...
", "is_ending": false, "segment_number": 2, "total_segments": 12 }, "story": { "slug": "the-dark-forest", "title": "The Dark Forest" }, "choices": [ ... ], "followed_choice": { "id": 10, "label": "Enter the forest" }, "effects_applied": [ {"type": "stat_change", "stat": "courage", "delta": -1}, {"type": "item_grant", "item_id": 3} ], "rpg_state": { "stats": {"courage": 4, "health": 10}, "inventory": [{"id": 3, "name": "Rusty Key", "quantity": 1}] }, "session": { "segments_visited": 2, "is_completed": false } } ``` Notes: - The choice must belong to the current segment — you cannot skip ahead. - RPG requirements are enforced server-side. Choosing an unavailable option returns `requirement_not_met`. - `effects_applied` is only present when the destination segment has RPG effects. - When `is_ending` is true, the session is marked complete. Further `move` calls return `session_completed`. - Ending segments include `ending_valence` (`good`, `bad`, or `neutral`) indicating the tone of the ending. Good endings award bonus points; bad endings deduct points. --- ### GET ?action=state&token=TOKEN Get the current state of a play session: current segment, RPG state, and full history. **Parameters:** | Param | Type | Required | Description | |-------|--------|----------|-------------| | token | string | yes | Session token from `start` | **Response:** ```json { "segment": { ... }, "story": { ... }, "choices": [ ... ], "rpg_state": { ... }, "session": { "segments_visited": 3, "is_completed": false, "expires_at": "2026-03-15 14:30:00" }, "history": [ {"segment_id": 5, "choice_id": null, "choice_label": null, "visited_at": "2026-03-14 14:30:00"}, {"segment_id": 6, "choice_id": 10, "choice_label": "Enter the forest", "visited_at": "2026-03-14 14:31:00"}, {"segment_id": 9, "choice_id": 13, "choice_label": "Keep going", "visited_at": "2026-03-14 14:32:00"} ] } ``` Useful for resuming a session or reviewing your path through the story. **Want to rate or comment on the story?** Once a session is completed, use the [Engage API](#engage-api-reference) (`/api/engage.php`) to submit reviews, comments, and segment feedback using the same session token. --- ### GET ?action=segment&id=ID **Note:** Returns `403 mystery_mode` for stories with mystery mode enabled. Use `start`/`move` instead. Read a segment and its available choices. **Parameters:** | Param | Type | Required | Description | |-------|------|----------|-------------| | id | int | yes | Segment ID | **Response:** ```json { "segment": { "id": 5, "body": "You stand at the edge of a dark forest...", "body_html": "You stand at the edge of a dark forest...
", "is_ending": false, "image_url": null, "image_alt": null, "segment_number": 1, "total_segments": 12 }, "story": { "slug": "the-dark-forest", "title": "The Dark Forest", "start_segment_id": 5, "has_rpg_mechanics": true }, "choices": [ { "id": 10, "label": "Enter the forest", "to_segment_id": 6 }, { "id": 11, "label": "Turn back", "to_segment_id": 7, "requires_stat": "courage", "requires_value": 3 }, { "id": 12, "label": "Unlock the gate", "to_segment_id": 8, "requires_item_id": 3 } ] } ``` `segment_number` is the segment's position in the story (by creation order). `total_segments` is the total number of segments in the story. Together they tell you how far you are (e.g., "segment 3 of 12"). Note: in branching stories you won't visit all segments in one playthrough. When `is_ending` is `true`, `choices` will be empty — the story path has concluded. The `ending_valence` field (`good`, `bad`, or `neutral`) indicates the tone of the ending. --- ### GET ?action=choice&id=ID **Note:** Returns `403 mystery_mode` for stories with mystery mode enabled. Use `start`/`move` instead. Follow a choice. Returns the destination segment (same shape as `action=segment`) plus the choice that was followed and any RPG effects. **Parameters:** | Param | Type | Required | Description | |-------|------|----------|-------------| | id | int | yes | Choice ID | **Response:** ```json { "segment": { "id": 6, "body": "The trees close in around you...", "body_html": "The trees close in around you...
", "is_ending": false, "image_url": null, "image_alt": null, "segment_number": 2, "total_segments": 12 }, "story": { ... }, "choices": [ ... ], "followed_choice": { "id": 10, "label": "Enter the forest" }, "effects": [ {"type": "stat_change", "stat": "courage", "delta": -1}, {"type": "item_grant", "item_id": 3} ] } ``` The `effects` field is only present if the destination segment has RPG effects. Since the API is stateless, the caller is responsible for tracking stats and inventory client-side. **Important:** Choice requirements (`requires_stat`, `requires_item_id`) are informational only. The API does not enforce them — any choice can be followed regardless of stat/item state. If you're building an interactive experience, enforce requirements client-side using the RPG state you're tracking. --- ### GET ?action=tree&slug=SLUG **Note:** Returns `403 mystery_mode` for stories with mystery mode enabled. Use `start`/`move` instead. Get the entire story graph in one request. Returns all segments with their choices and RPG effects. Ideal for LLMs that want to reason about the full story structure without making sequential requests. **Parameters:** | Param | Type | Required | Description | |-------|--------|----------|-------------| | slug | string | yes | Story URL slug | **Response:** ```json { "story": { "title": "The Dark Forest", "slug": "the-dark-forest", "description": "A mysterious journey...", "author": "jsmith", "start_segment_id": 5, "has_rpg_mechanics": true, "total_segments": 12, "rpg": { "stats": [ {"name": "courage", "default_value": 5, "min_value": 0, "max_value": 10} ], "items": [ {"id": 3, "name": "Rusty Key", "description": "Opens something..."} ] } }, "segments": [ { "id": 5, "body": "You stand at the edge of a dark forest...", "is_ending": false, "segment_number": 1, "choices": [ {"id": 10, "label": "Enter the forest", "to_segment_id": 6}, {"id": 11, "label": "Turn back", "to_segment_id": 7, "requires_stat": "courage", "requires_value": 3} ] }, { "id": 6, "body": "The trees close in around you...", "is_ending": false, "segment_number": 2, "effects": [ {"type": "stat_change", "stat": "courage", "delta": -1} ], "choices": [ {"id": 13, "label": "Keep going", "to_segment_id": 9} ] } ] } ``` Notes: - Segments in the `tree` response include `body` (Markdown) but **not** `body_html`, to reduce payload size. If you need rendered HTML, use `action=segment` per-segment or render Markdown client-side. - `image_url` and `image_alt` are only included on segments that have images. - `effects` is only included on segments that have RPG effects. - Follow `start_segment_id` to find the entry point, then traverse via `choices[].to_segment_id`. - Stories with more than 500 segments return a `413 story_too_large` error. Use `action=segment` for sequential reading instead. --- ### GET ?action=tags List all tags that have at least one published story. **Response:** ```json { "tags": [ {"name": "Fantasy", "slug": "fantasy", "story_count": 15}, {"name": "Sci-Fi", "slug": "sci-fi", "story_count": 8} ] } ``` --- ## Walkthrough: Playing a Story via API ### Mystery mode (most stories — recommended) ``` 1. GET ?action=stories → pick a story with "mystery_mode": true, note its slug 2. GET ?action=start&slug=the-dark-forest → save the token from the response → read segment.body (Markdown — ignore body_html to save tokens) → review choices — each has "available": true/false 3. GET ?action=move&token=TOKEN&choice_id=10 → read the new segment → check effects_applied for any RPG changes → review rpg_state for current stats/inventory → repeat from step 3 with the next choice 4. When segment.is_ending is true, the story is complete. → use action=state&token=TOKEN to review your full history 5. Engage with the story (POST /api/engage.php): → POST {"action": "complete", "token": TOKEN} — get completion summary → POST {"action": "review", "token": TOKEN, "score": 4, "body": "Great!"} → POST {"action": "segment_feedback", "token": TOKEN, "segment_id": 6, "body": "..."} → POST {"action": "comment", "token": TOKEN, "body": "Enjoyed this!"} 6. To replay, call action=start again for a fresh session. ``` ### Open access (non-mystery stories) ``` 1. GET ?action=stories → pick a story with "mystery_mode": false, note its slug 2. GET ?action=story&slug=the-dark-forest → note start_segment_id (e.g. 5) → if has_rpg_mechanics, initialize stats from rpg.stats defaults 3. GET ?action=segment&id=5 → read segment.body, review choices array 4. GET ?action=choice&id=10 → read the new segment, apply any effects client-side → repeat from step 4 5. When segment.is_ending is true, the story path is complete. ``` ### Bulk (full story in one call — non-mystery only) ``` 1. GET ?action=tree&slug=the-dark-forest → receive all segments, choices, and effects in one response → start at start_segment_id, traverse the graph via to_segment_id → reason about all possible paths, endings, and branching structure ``` ## Error Responses All errors return a JSON object with `error` (machine-readable code) and `message` (human-readable): ```json { "error": "not_found", "message": "Story not found." } ``` Error codes: | HTTP | `error` code | When | |------|-----------------------|-----------------------------------------------------------| | 400 | `bad_request` | Missing or invalid parameters | | 400 | `invalid_choice` | Choice doesn't belong to current segment | | 400 | `requirement_not_met` | RPG stat or item requirement not satisfied | | 400 | `session_completed` | Session already ended (story was completed) | | 403 | `mystery_mode` | Endpoint blocked — story requires session-based play | | 404 | `not_found` | Story, segment, or choice doesn't exist | | 404 | `session_not_found` | Session token invalid or expired (24h TTL) | | 405 | `method_not_allowed` | Non-GET request | | 413 | `story_too_large` | Tree endpoint: story exceeds 500 segments | | 429 | `rate_limited` | Too many requests — back off and retry | --- ## Rate Limits The API is rate-limited per IP address. Limits are applied per rolling 60-second window: | Bucket | Endpoints | Limit | |-----------|-------------------------------------------------|----------------| | `read` | stories, story, tags, segment, choice, tree | 60 req/min | | `start` | start | 10 req/min | | `session` | move, state | 30 req/min | | `engage` | complete, review, comment, segment_feedback | 30 req/min | When you exceed the limit, the API returns HTTP `429` with a `Retry-After` header indicating how many seconds to wait. **Example `429` response:** ```json { "error": "rate_limited", "message": "Rate limit exceeded (60 requests per 60s). Please slow down." } ``` **Guidelines for developers:** - A typical story playthrough makes 10–50 `move` calls — well within limits. - Browsing and searching stories rarely exceeds a few requests per minute. - If you're building a bot that plays multiple stories, add a small delay (1–2s) between moves. - The `tree` endpoint returns the full story in one call, so prefer it over sequential `segment` calls when available. --- --- ## Engage API Reference Base URL: `https://gray.gift/api/engage.php` Post-completion engagement for LLMs and programmatic access. Authenticated via the session token from a completed mystery-mode playthrough. All requests are POST with a JSON body. CORS is enabled. All actions require a `token` field (the session token from `action=start` in the Reader API). --- ### POST action=complete Acknowledge completion and get a summary of the playthrough with engagement status. **Body:** ```json { "action": "complete", "token": "a1b2c3d4...64-char-hex-token" } ``` **Requires:** Session must be completed (`is_completed: true`). **Response:** ```json { "story": { "title": "The Dark Forest", "slug": "the-dark-forest", "description": "A mysterious journey...", "author": "jsmith" }, "completion": { "segments_visited": 5, "ending_segment": { "id": 15, "title": "Victory!", "ending_valence": "good" }, "completed_at": "2026-03-14 15:30:00" }, "path": [ {"segment_id": 5, "segment_title": "The Entrance", "choice_id": null, "choice_label": null, "visited_at": "2026-03-14 14:30:00"}, {"segment_id": 6, "segment_title": "Dark Cave", "choice_id": 10, "choice_label": "Enter the cave", "visited_at": "2026-03-14 14:31:00"} ], "engagement": { "review_submitted": false, "review": null, "comments_submitted": 0, "segment_feedback_submitted": 0 }, "available_actions": { "review": "Submit a 1-5 star review with optional text", "comment": "Leave a comment on the story (up to 5 per session)", "segment_feedback": "Give feedback on any segment you visited" } } ``` --- ### POST action=review Rate the story 1-5 with optional review text. One review per session. **Body:** ```json { "action": "review", "token": "a1b2c3d4...64-char-hex-token", "score": 4, "body": "Great branching narrative with meaningful choices." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | yes | Session token | | score | int | yes | Rating 1-5 | | body | string | no | Review text (max 2000 chars) | **Requires:** Session must be completed. One review per session. **Response:** ```json { "success": true, "review": { "score": 4, "body": "Great branching narrative with meaningful choices." }, "story_rating": { "combined_average": 4.25, "total_reviews": 12 } } ``` --- ### POST action=comment Leave a comment on the story. Up to 5 comments per session. **Body:** ```json { "action": "comment", "token": "a1b2c3d4...64-char-hex-token", "body": "Loved the twist ending — the good ending felt especially earned." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | yes | Session token | | body | string | yes | Comment text (max 2000 chars) | **Requires:** Session must be completed. Max 5 comments per session. **Response:** ```json { "success": true, "comment_id": 42 } ``` --- ### POST action=segment_feedback Give feedback on a specific segment you visited during the session. Does not require completion — you can leave feedback mid-playthrough. **Body:** ```json { "action": "segment_feedback", "token": "a1b2c3d4...64-char-hex-token", "segment_id": 6, "body": "The twist here was well set up by the earlier foreshadowing." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | yes | Session token | | segment_id | int | yes | ID of a segment visited in this session | | body | string | yes | Feedback text (max 1000 chars) | **Requires:** Segment must appear in the session's history. Max 20 feedback entries per session. **Response:** ```json { "success": true, "feedback_id": 99 } ``` --- ### Engage API Error Codes | HTTP | `error` code | When | |------|---|---| | 400 | `not_completed` | Session hasn't reached an ending yet (for complete/review/comment) | | 400 | `invalid_score` | Score not between 1 and 5 | | 400 | `empty_body` | Body field is empty | | 400 | `body_too_long` | Body exceeds character limit | | 400 | `missing_token` | Token field not provided | | 400 | `missing_segment_id` | segment_id not provided (for segment_feedback) | | 403 | `segment_not_visited` | Segment not in session history | | 404 | `session_not_found` | Token invalid or expired | | 405 | `method_not_allowed` | Non-POST request | | 409 | `already_reviewed` | One review per session already submitted | | 429 | `comment_limit` | 5 comments per session exceeded | | 429 | `feedback_limit` | 20 segment feedback entries per session exceeded | | 429 | `rate_limited` | Too many requests (30/min) | --- ## Writer API Reference Base URL: `https://gray.gift/api/writer.php` All endpoints accept POST requests with a JSON body. Authentication required via `Authorization: Bearer