# Quote Generator API Generate Twitter-style quote images programmatically. Perfect for creating social media content, screenshots, or fake tweets. **Base URL:** `https://quotes.imbenji.net` ## Features - Twitter-authentic styling - Support for images, videos, GIFs, and text-only tweets - Verified badge (blue checkmark) - Engagement metrics (likes, retweets, replies, views) - High-resolution output (3240x3240) - Built-in caching (24-hour TTL) - Base64 image support - Video/GIF rendering with Playwright (v2 API only) - Background video processing with status tracking - Docker-ready - v2 API with stateful sessions for incremental updates - Persistent snapshot links (48-hour TTL, immutable, shareable) ## API Endpoints ### POST /generate Generate a quote image using JSON request body. **Request:** ```bash curl -X POST https://quotes.imbenji.net/generate \ -H "Content-Type: application/json" \ -d '{ "displayName": "Geoff Marshall", "username": "@geofftech", "avatarUrl": "https://example.com/avatar.jpg", "text": "Does anyone else find it immensely satisfying when you turn a pocket inside out and get rid of the crumbs and fluff stuck in the bottom.", "timestamp": 1499766270 }' --output quote.png ``` ### GET /generate Generate a quote image using query parameters. **Request:** ```bash curl "https://quotes.imbenji.net/generate?displayName=John%20Doe&username=@johndoe&text=Hello%20World×tamp=1735574400" --output quote.png ``` ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `displayName` | string | No | Display name (defaults to "Anonymous") | | `username` | string | No | Twitter handle with @ (defaults to "@anonymous") | | `avatarUrl` | string | No | Avatar image URL or base64 data URI | | `text` | string | No | Tweet text content | | `imageUrl` | string | No | Tweet image URL, base64 data URI, or `null` | | `timestamp` | integer | No | Unix epoch timestamp in seconds | | `verified` | boolean | No | Show verified badge (blue checkmark) next to name | | `engagement` | object | No | Engagement metrics (likes, retweets, replies, views) | ## Response - **Content-Type:** `image/png` - **Headers:** - `X-Cache`: `HIT` (served from cache) or `MISS` (newly generated) ## Examples ### Text-only tweet ```javascript fetch('https://quotes.imbenji.net/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'John Doe', username: '@johndoe', text: 'Just deployed my new API!', timestamp: Math.floor(Date.now() / 1000) }) }) .then(res => res.blob()) .then(blob => { const url = URL.createObjectURL(blob); document.getElementById('img').src = url; }); ``` ### Tweet with image ```javascript const response = await fetch('https://quotes.imbenji.net/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Jane Smith', username: '@janesmith', avatarUrl: 'https://example.com/avatar.jpg', text: 'Check out this amazing view! ๐ŸŒ„', imageUrl: 'https://example.com/photo.jpg', timestamp: 1735574400 }) }); const blob = await response.blob(); // Save or display the image ``` ### Using base64 images ```javascript fetch('https://quotes.imbenji.net/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Alice', username: '@alice', avatarUrl: 'data:image/png;base64,iVBORw0KGgo...', text: 'Using base64 images works too!', imageUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJ...', timestamp: 1735574400 }) }); ``` ### Python example ```python import requests import time response = requests.post('https://quotes.imbenji.net/generate', json={ 'displayName': 'Python User', 'username': '@pythonista', 'text': 'Making API calls with Python!', 'timestamp': int(time.time()) }) with open('quote.png', 'wb') as f: f.write(response.content) ``` ### cURL with GET ```bash curl -G "https://quotes.imbenji.net/generate" \ --data-urlencode "displayName=Test User" \ --data-urlencode "username=@testuser" \ --data-urlencode "text=This is a test tweet" \ --data-urlencode "timestamp=1735574400" \ --output quote.png ``` ### Verified badge ```javascript fetch('https://quotes.imbenji.net/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Elon Musk', username: '@elonmusk', text: 'Just bought Twitter!', verified: true, timestamp: 1735574400 }) }); ``` ### Engagement metrics ```javascript fetch('https://quotes.imbenji.net/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Popular User', username: '@viral', text: 'This tweet went viral!', verified: true, engagement: { replies: 1234, retweets: 5678, likes: 98765, views: 1500000 }, timestamp: 1735574400 }) }); ``` **Note:** All four engagement fields (replies, retweets, likes, views) must be provided for the engagement bar to appear. Numbers are automatically formatted (e.g., 1234 โ†’ 1.2K, 1500000 โ†’ 1.5M). --- ## v2 API (Stateful Sessions) The v2 API lets you build quote images incrementally. Instead of sending everything in one request, you create a session and update fields as needed. This is more efficient when making multiple edits since you dont need to resend large images every time. ### How it works 1. Create a session with `POST /v2/quote` 2. Update fields with `PATCH /v2/quote/:id` (only send whats changed) 3. Render the image with `GET /v2/quote/:id/image` Sessions expire after 24 hours of inactivity. ### Video/GIF Support (v2 Only) The v2 API supports video and animated GIF rendering. When you provide a video or GIF in the `imageUrl` parameter, the API automatically: - Detects the video format (MP4, WebM, MOV, GIF) - Renders the video in the background using Playwright - Returns an animated MP4 file showing the video playing within the tweet frame - Tracks render status (`pending`, `rendering`, `completed`, `failed`) **Supported formats:** - MP4 (H.264, H.265) - WebM (VP8, VP9) - MOV (QuickTime) - GIF (animated, converted to MP4) **Note:** Video rendering happens asynchronously in the background. The `GET /v2/quote/:id/image` endpoint will wait (up to 15 minutes) for the render to complete before returning the video file. ### POST /v2/quote Create a new session. **Request:** ```bash curl -X POST https://quotes.imbenji.net/v2/quote \ -H "Content-Type: application/json" \ -d '{ "displayName": "John Doe", "username": "@johndoe", "text": "Hello world" }' ``` **Response (201):** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "displayName": "John Doe", "username": "@johndoe", "text": "Hello world", "avatarUrl": null, "imageUrl": null, "timestamp": null, "verified": false, "engagement": null, "renderStatus": "completed", "renderProgress": 100, "renderPid": null, "renderError": null, "createdAt": 1735600000, "updatedAt": 1735600000 } ``` **Render Status Fields:** - `renderStatus`: Current render state (`pending`, `rendering`, `completed`, `failed`) - `renderProgress`: Render progress percentage (0-100) - `renderPid`: Process ID of background render (only present during `rendering`) - `renderError`: Error message if `renderStatus` is `failed` ### GET /v2/quote/:id Get current session state. ```bash curl https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` ### PATCH /v2/quote/:id Update specific fields. Only send the fields you want to change. ```bash curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Content-Type: application/json" \ -d '{ "text": "Updated text!", "avatarUrl": "data:image/png;base64,..." }' ``` **Engagement updates:** ```bash # Set all engagement metrics curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Content-Type: application/json" \ -d '{ "engagement": { "replies": 100, "retweets": 250, "likes": 5000, "views": 50000 } }' # Partial update (update only likes, keep other fields) curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Content-Type: application/json" \ -d '{ "engagement": { "likes": 10000 } }' # Clear engagement entirely (removes engagement bar) curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Content-Type: application/json" \ -d '{ "engagement": null }' ``` **Verified badge:** ```bash # Add verified badge curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Content-Type: application/json" \ -d '{ "verified": true }' ``` ### GET /v2/quote/:id/render-info Get real-time render progress via Server-Sent Events (SSE). ```bash curl -N https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/render-info ``` **Response (text/event-stream):** ``` data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":0,"stage":"rendering","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":25,"stage":"rendering","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":50,"stage":"rendering","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":99,"stage":"rendering","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":0,"stage":"encoding","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":50,"stage":"encoding","error":null} data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":99,"stage":"encoding","error":null} data: {"status":"completed","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":100,"stage":"completed","error":null} ``` **JavaScript Example:** ```javascript const eventSource = new EventSource( `https://quotes.imbenji.net/v2/quote/${sessionId}/render-info` ); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log(`Render: ${data.stage} (${data.progress}%)`); // Update progress bar and stage label in UI document.getElementById('progress-bar').style.width = `${data.progress}%`; document.getElementById('progress-text').textContent = `${data.progress}%`; // Show current stage let stageText = ''; if (data.stage === 'rendering') { stageText = 'Rendering video...'; } else if (data.stage === 'encoding') { stageText = 'Encoding to H.264...'; } else if (data.stage === 'completed') { stageText = 'Complete!'; } document.getElementById('stage-label').textContent = stageText; if (data.status === 'completed') { console.log('Render complete! Fetching video...'); eventSource.close(); // Now fetch the video fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}/image`) .then(res => res.blob()) .then(blob => { // Handle the video blob }); } else if (data.status === 'failed') { console.error('Render failed:', data.error); eventSource.close(); } }; eventSource.onerror = (error) => { console.error('SSE error:', error); eventSource.close(); }; ``` **Status values:** - `pending` - Render queued but not started (progress: 0%) - `rendering` - In progress (progress: 0-99%) - `completed` - Successfully rendered (progress: 100%) - `failed` - Render failed (check `error` field) - `deleted` - Session was deleted **Response fields:** - `status` - Current render status (`pending`, `rendering`, `completed`, `failed`) - `sessionId` - Session identifier - `progress` - Current stage completion percentage (0-100) - `stage` - Current render stage (`idle`, `rendering`, `encoding`, `completed`, `failed`) - `error` - Error message (only present if status is `failed`) **Render Stages:** 1. **rendering** - Recording video with Playwright (progress 0-99%) 2. **encoding** - Converting to H.264 MP4 with FFmpeg (progress 0-99%) 3. **completed** - Render finished successfully (progress 100%) **Notes:** - The stream automatically closes when render is `completed` or `failed` - Progress updates are sent every 200ms while render is in progress - Progress resets to 0% when transitioning between stages (rendering โ†’ encoding) - Each stage reports its own progress percentage independently - Use the `stage` field to show different messages in your UI ### GET /v2/quote/:id/image Render the current session state as an image or video. ```bash # For images curl https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/image --output quote.png # For videos curl https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/image --output quote.mp4 ``` **Response headers:** - `Content-Type: image/png` (for images) or `video/mp4` (for videos) - `X-Session-Id: ` - `X-Cache: HIT` or `MISS` **Notes:** - For video renders, this endpoint will wait up to 15 minutes for the background render to complete - If the render is still in progress, the response will be delayed (polling internally) - If the render fails, a 500 error will be returned with details - If the render times out, a 504 error will be returned - For better UX, use `/render-info` to track progress and only call this when complete ### DELETE /v2/quote/:id Delete a session. ```bash curl -X DELETE https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` Returns `204 No Content` on success. ### POST /v2/quote/:id/snapshot-link Create a persistent snapshot link. This captures the current state of your session and generates a shareable URL that persists for 48 hours (refreshing on each access). Unlike the regular `/image` endpoint, snapshots are immutable - they always show the image as it was when the snapshot was created, even if you update the session afterwards. **Request:** ```bash curl -X POST https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/snapshot-link ``` **Response (201):** ```json { "token": "xY9pQmN3kL8vFw2jRtZ7", "url": "https://quotes.imbenji.net/v2/snapshot/xY9pQmN3kL8vFw2jRtZ7", "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "createdAt": 1735600000, "expiresAt": 1735772800 } ``` ### GET /v2/snapshot/:token Retrieve a snapshot image using its token. ```bash curl https://quotes.imbenji.net/v2/snapshot/xY9pQmN3kL8vFw2jRtZ7 --output snapshot.png ``` **Response headers:** - `Content-Type: image/png` - `X-Snapshot-Token: ` - `X-Cache: HIT` or `MISS` **Snapshot behavior:** - **Immutable:** Shows the session state when the snapshot was created - **TTL:** 48 hours from last access (resets on each view) - **Cascade delete:** Deleted automatically when parent session is deleted - **Shareable:** Token can be shared with anyone ### v2 Example Workflow ```javascript // 1. Create session with initial data const res = await fetch('https://quotes.imbenji.net/v2/quote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Jane Doe', username: '@janedoe', text: 'Working on something cool' }) }); const session = await res.json(); const sessionId = session.id; // 2. Later, add an avatar (only sends the avatar, not everything again) await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ avatarUrl: 'data:image/png;base64,iVBORw0KGgo...' }) }); // 3. Update the text await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Finished building something cool!' }) }); // 4. Generate the final image const imgRes = await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}/image`); const blob = await imgRes.blob(); // 5. (Optional) Create a shareable snapshot link const snapshotRes = await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}/snapshot-link`, { method: 'POST' }); const snapshot = await snapshotRes.json(); console.log('Shareable URL:', snapshot.url); // Anyone can access: https://quotes.imbenji.net/v2/snapshot/ ``` ### v2 Video Example ```javascript // 1. Create session with video const res = await fetch('https://quotes.imbenji.net/v2/quote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Video Creator', username: '@videocreator', text: 'Check out this awesome clip!', imageUrl: 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21...' // base64 video }) }); const session = await res.json(); console.log('Render status:', session.renderStatus); // "rendering" // 2. Track render progress with SSE (recommended) const eventSource = new EventSource( `https://quotes.imbenji.net/v2/quote/${session.id}/render-info` ); eventSource.onmessage = async (event) => { const data = JSON.parse(event.data); console.log(`Render: ${data.status} (${data.progress}%)`); // Update UI with progress if (data.status === 'rendering') { // Show progress bar: 0% -> 99% updateProgressBar(data.progress); } if (data.status === 'completed') { eventSource.close(); updateProgressBar(100); // 3. Get the rendered video const videoRes = await fetch(`https://quotes.imbenji.net/v2/quote/${session.id}/image`); const videoBlob = await videoRes.blob(); console.log('Content-Type:', videoRes.headers.get('Content-Type')); // "video/mp4" // Save the video const url = URL.createObjectURL(videoBlob); const a = document.createElement('a'); a.href = url; a.download = 'quote-video.mp4'; a.click(); } else if (data.status === 'failed') { console.error('Render failed:', data.error); eventSource.close(); } }; // Alternative: Simple approach (blocks until complete) // const videoRes = await fetch(`https://quotes.imbenji.net/v2/quote/${session.id}/image`); // const videoBlob = await videoRes.blob(); ``` **GIF Example:** ```javascript // Animated GIFs are automatically detected and rendered as MP4 videos const res = await fetch('https://quotes.imbenji.net/v2/quote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'GIF Lover', username: '@giflover', text: 'This GIF is fire ๐Ÿ”ฅ', imageUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAP...' // animated GIF }) }); // The output will be an MP4 video file ``` **Full HTML Progress Bar Example:** ```html
0%
``` ### v2 Parameters | Parameter | Type | Description | |-----------|------|-------------| | `displayName` | string | Display name | | `username` | string | Twitter handle with @ | | `avatarUrl` | string | Avatar image URL or base64 | | `text` | string | Tweet text content | | `imageUrl` | string | Tweet image URL or base64 | | `timestamp` | integer | Unix epoch in seconds | | `verified` | boolean | Show verified badge (blue checkmark) | | `engagement` | object/null | Engagement metrics (see below) | **Engagement object:** ```json { "engagement": { "replies": 89, "retweets": 567, "likes": 1234, "views": 50000 } } ``` **Engagement behavior:** - All four fields must be provided for the engagement bar to appear - Partial updates: Only update specific fields, others remain unchanged - Set to `null` to clear all engagement and hide the engagement bar - Numbers are auto-formatted (1234 โ†’ 1.2K, 1000000 โ†’ 1M) --- ## Caching The API automatically caches generated images for 24 hours from the last request. Identical requests will be served from cache instantly. - Cache hits include `X-Cache: HIT` header - Cache misses include `X-Cache: MISS` header - Cache is cleaned up hourly - Images are stored based on SHA-256 hash of parameters ## Timestamp Format The `timestamp` parameter expects a Unix epoch timestamp in **seconds** (not milliseconds). **JavaScript:** ```javascript const timestamp = Math.floor(Date.now() / 1000); ``` **Python:** ```python import time timestamp = int(time.time()) ``` The timestamp will be formatted as: `5:58 PM ยท Dec 29, 2025` ## Default Avatar If no `avatarUrl` is provided, a default Twitter-style placeholder avatar will be used. ## Rate Limiting Currently, there are no rate limits. Please use responsibly. ## Self-Hosting ### Docker (Recommended) ```bash git clone cd quotegen docker-compose up -d ``` The API will be available at `http://localhost:3000` ### Manual Setup ```bash npm install npx playwright install chromium npm start ``` **Note:** Video rendering requires Playwright and FFmpeg. The Docker setup handles this automatically. ## Health Check ```bash curl https://quotes.imbenji.net/health ``` Response: ```json { "status": "ok" } ``` ## Technical Details - **Resolution:** 3240x3240 (1:1 aspect ratio) for images, 1500x1500 for videos - **Format:** PNG (images), MP4 (videos) - **Max tweet width:** 450px (auto-scaled to fit) - **Cache TTL:** 24 hours from last access - **Session TTL:** 24 hours from last update (v2 API) - **Snapshot TTL:** 48 hours from last access (v2 API) - **Cleanup interval:** Every hour - **Database:** SQLite (for v2 sessions and snapshots) - **Video rendering:** Playwright Chromium with background worker processes - **Video timeout:** 15 minutes maximum render time - **Supported video formats:** MP4, WebM, MOV, GIF ## Limitations - Tweet text is not wrapped intelligently (use `\n` for line breaks if needed) - Very long tweets may be cut off - External image URLs must be publicly accessible - Base64 images should be reasonably sized - Video rendering is only available in v2 API (not in `/generate` endpoint) - Video renders can take several seconds depending on video duration - Very long videos (>15 minutes) may timeout - Video audio is muted in the output ## Support For issues or questions, please contact the maintainer. --- **Built with:** Node.js, Express, Puppeteer, Playwright, SQLite, FFmpeg