| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A headless HTML sandbox — publish and serve HTML documents via a simple API.
ZenBin is a lightweight web service that lets you publish HTML documents to unique IDs and view them at predictable URLs. It's optimized for fast sharing, demos, prototypes, and lightweight hosting of single-page HTML documents.
# Install dependencies
npm install
# Run in development mode
npm run dev
# Or build and run in production
npm run build
npm startThe server starts at http://localhost:3000 by default.
POST /v1/pages/{id}
Content-Type: application/jsonRequest body:
{
"html": "<!doctype html><html><body>Hello World</body></html>",
"markdown": "# Hello World\n\nThis is the markdown source.",
"image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"title": "My Page",
"encoding": "utf-8",
"markdown_encoding": "utf-8",
"content_type": "text/html; charset=utf-8"
}| Field | Required | Description |
|---|---|---|
| html | No* | HTML content (string). *At least one of html, markdown, or image is required. |
| markdown | No* | Markdown source content (string). *At least one of html, markdown, or image is required. |
| image | No* | Base64-encoded image data. *At least one of html, markdown, or image is required. |
| title | No | Page title (metadata) |
| encoding | No | utf-8 (default) or base64 for the html field |
| markdown_encoding | No | utf-8 (default) or base64 for the markdown field |
| content_type | No | Content-Type header (default: text/html; charset=utf-8). Required for image pages. |
| auth | No | Authentication settings (see Page Authentication) |
Response:
{
"id": "my-page",
"url": "http://localhost:3000/p/my-page",
"raw_url": "http://localhost:3000/p/my-page/raw",
"markdown_url": "http://localhost:3000/p/my-page/md",
"etag": "\"abc123...\""
}Subdomain pages: Just POST again with the same ID and X-Subdomain header. No special flag needed.
Non-subdomain pages: Add ?overwrite=true query parameter:
POST /v1/pages/my-page?overwrite=truePassword-protected pages: Include Basic Auth header when updating:
curl -X POST http://localhost:3000/v1/pages/secret?overwrite=true \
-H "Content-Type: application/json" \
-u ":mypassword123" \
-d '{"html": "<h1>Updated</h1>"}'DELETE /v1/pages/{id}For password-protected pages, include Basic Auth. For subdomain pages, include X-Subdomain header.
GET /p/{id}Returns the HTML page with security headers applied. Supports If-None-Match for caching (returns 304 Not Modified if unchanged).
GET /p/{id}/rawReturns the raw HTML as text/plain with a Content-Disposition header for downloading.
GET /p/{id}/mdReturns the markdown source as text/markdown. Returns 404 if the page has no markdown content.
Alternatively, request markdown via content negotiation:
GET /p/{id}
Accept: text/markdownIf a page is created with only markdown (no HTML), GET /p/{id} automatically returns the markdown content with Content-Type: text/markdown.
Upload images up to 5MB. Supported formats: PNG, JPEG, GIF, WebP, SVG.
Create an image page:
# Encode image to base64
IMAGE_BASE64=$(base64 -i myimage.png | tr -d '\n')
curl -X POST http://localhost:3000/v1/pages/my-logo \
-H "Content-Type: application/json" \
-d "{
\"image\": \"$IMAGE_BASE64\",
\"content_type\": \"image/png\"
}"Access the image:
# View in browser
GET /p/my-logo
# Explicit image endpoint
GET /p/my-logo/imageThe image is served with the correct Content-Type header. When a page has both html and image, the HTML is served at /p/{id} and the image at /p/{id}/image.
GET /healthReturns {"status": "ok", "timestamp": "..."}.
GET /api/agentReturns markdown instructions for AI agents on how to use the API.
POST /api/proxy
Content-Type: application/jsonAllows ZenBin-hosted pages to make external HTTP requests through the server, bypassing CORS restrictions.
Request body:
{
"url": "https://api.example.com/data",
"method": "GET",
"auth": {
"type": "bearer",
"credentials": "your-token"
}
}| Field | Required | Description |
|---|---|---|
| url | Yes | Target URL (http/https only) |
| method | No | HTTP method (default: GET) |
| body | No | Request body to forward (JSON) |
| timeout | No | Timeout in ms (max: 30000) |
| contentType | No | Content-Type for outgoing request |
| accept | No | Accept header for outgoing request |
| auth | No | Authentication config (see below) |
Authentication types:
| Type | Usage | Result Header |
|---|---|---|
| bearer | { type: "bearer", credentials: "token" } | Authorization: Bearer token |
| basic | { type: "basic", credentials: "base64" } | Authorization: Basic base64 |
| api-key | { type: "api-key", credentials: "key", headerName: "X-API-Key" } | X-API-Key: key |
Response:
{
"status": 200,
"statusText": "OK",
"headers": { "content-type": "application/json" },
"body": { ... }
}Security: Only requests originating from ZenBin-hosted pages are allowed. SSRF protection blocks private IPs and internal endpoints.
curl -X POST http://localhost:3000/v1/pages/hello \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello World</h1>"}'curl -X POST http://localhost:3000/v1/pages/demo \
-H "Content-Type: application/json" \
-d '{
"html": "<!DOCTYPE html><html><head><style>body{font-family:sans-serif;padding:2rem}</style></head><body><h1>Demo</h1><p>This is a demo page.</p></body></html>",
"title": "Demo Page"
}'# Encode your HTML
HTML_BASE64=$(echo -n '<h1>Encoded</h1>' | base64)
curl -X POST http://localhost:3000/v1/pages/encoded \
-H "Content-Type: application/json" \
-d "{\"encoding\":\"base64\",\"html\":\"$HTML_BASE64\"}"curl -X POST http://localhost:3000/v1/pages/article \
-H "Content-Type: application/json" \
-d '{
"html": "<!DOCTYPE html><html><head><style>body{font-family:system-ui;max-width:700px;margin:0 auto;padding:2rem}</style></head><body><h1>My Article</h1><p>Content here.</p></body></html>",
"markdown": "# My Article\n\nContent here.",
"title": "My Article"
}'Response includes markdown URL:
{
"id": "article",
"url": "http://localhost:3000/p/article",
"raw_url": "http://localhost:3000/p/article/raw",
"markdown_url": "http://localhost:3000/p/article/md",
"etag": "\"...\""
}curl -X POST http://localhost:3000/v1/pages/notes \
-H "Content-Type: application/json" \
-d '{
"markdown": "# Notes\n\n- Item 1\n- Item 2\n- Item 3"
}'Pages are public by default. You can optionally protect pages with password authentication, secret URL tokens, or both.
Add HTTP Basic Auth to require a password when viewing the page:
curl -X POST http://localhost:3000/v1/pages/secret \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Secret Page</h1>",
"auth": { "password": "mypassword123" }
}'Browsers will automatically prompt for the password. With curl:
curl -u ":mypassword123" http://localhost:3000/p/secretGenerate a secret shareable URL that grants access without a password:
curl -X POST http://localhost:3000/v1/pages/shared \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Shared Page</h1>",
"auth": { "urlToken": true }
}'Response includes secret URLs:
{
"id": "shared",
"url": "http://localhost:3000/p/shared",
"secret_url": "http://localhost:3000/p/shared?token=abc123...",
"secret_raw_url": "http://localhost:3000/p/shared/raw?token=abc123..."
}If the page has markdown, the response also includes secret_markdown_url:
{
"secret_markdown_url": "http://localhost:3000/p/shared/md?token=abc123..."
}Note: The token is only returned once at creation and cannot be retrieved later.
Use both password and URL token for maximum flexibility:
curl -X POST http://localhost:3000/v1/pages/dual \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Dual Auth</h1>",
"auth": { "password": "mypassword123", "urlToken": true }
}'Protected pages have built-in brute-force protection:
Configure via environment variables or .env file:
| Variable | Default | Description |
|---|---|---|
| PORT | 3000 | Server port |
| HOST | 0.0.0.0 | Server host |
| BASE_URL | http://localhost:3000 | Base URL for generated links |
| LMDB_PATH | ./data/zenbin.lmdb | Database path |
| MAX_PAYLOAD_SIZE | 524288 | Max HTML/markdown size in bytes (512KB) |
| MAX_IMAGE_SIZE | 5242880 | Max image size in bytes (5MB) |
| MAX_ID_LENGTH | 128 | Max page ID length |
| RATE_LIMIT_WINDOW_MS | 60000 | Rate limit window (ms) |
| RATE_LIMIT_MAX_REQUESTS | 100 | Max requests per window |
| PROXY_TIMEOUT_MS | 30000 | Max timeout for proxy requests |
| PROXY_MAX_REQUEST_SIZE | 5242880 | Max proxy request body (5MB) |
| PROXY_MAX_RESPONSE_SIZE | 5242880 | Max proxy response size (5MB) |
| PROXY_ALLOWED_DOMAINS | `` | Comma-separated domain allowlist (empty = all) |
| PROXY_RATE_LIMIT_MAX | 5 | Max proxy requests per window |
| PROXY_RATE_LIMIT_WINDOW_MS | 60000 | Proxy rate limit window (ms) |
| PROXY_MAX_REDIRECTS | 3 | Max redirects to follow |
| AUTH_BCRYPT_ROUNDS | 10 | bcrypt cost factor for password hashing |
| AUTH_TOKEN_LENGTH | 32 | URL token length in bytes (64 hex chars) |
| AUTH_MIN_PASSWORD_LENGTH | 8 | Minimum password length |
| AUTH_MAX_FAILED_ATTEMPTS | 5 | Max failed auth attempts before lockout |
| AUTH_FAILED_ATTEMPT_WINDOW_MS | 900000 | Failed attempt tracking window (15 min) |
| AUTH_LOCKOUT_DURATION_MS | 900000 | Lockout duration after max failures (15 min) |
| FREE_TIER_MONTHLY_LIMIT | 100 | Free tier request limit per 30 days |
| FREE_TIER_WINDOW_MS | 2592000000 | Free tier window in ms (30 days) |
| SUBDOMAINS_ENABLED | true | Enable subdomain feature |
| SUBDOMAIN_MAX_LENGTH | 63 | Max subdomain name length |
| SUBDOMAIN_MAX_PAGES | 100 | Max pages per subdomain |
| SUBDOMAIN_RESERVED_NAMES | (see config) | Comma-separated reserved names |
| SUBDOMAIN_BASE_DOMAIN | zenbin.org | Base domain for subdomains |
Page IDs must:
Valid examples: my-page, demo.v2, user_123, Report-2024.01
Pages are served with restrictive security headers:
ZenBin includes a render.yaml Blueprint for easy deployment to Render.com.
The Blueprint configures:
Due to Render's persistent disk constraints:
For multi-instance deployments, consider replacing LMDB with Render Postgres or Redis.
# Run with hot reload
npm run dev
# Run tests
npm test
# Build for production
npm run buildMIT
| Back | FazBrowse Home | New Git URL |