Executive Overview
Trail is a production-ready, multi-user link journaling API service that enables users to create, share, and engage with short-form content entries (140 characters) with optional media attachments and automatic URL preview enrichment.
Key Capabilities
System Architecture
Quick Stats
Quick Start Guide
Public API Usage (No Authentication)
# List all public entries
curl https://trail.services.kibotu.net/api/entries?limit=20
# Search entries
curl https://trail.services.kibotu.net/api/entries?q=example&limit=20
# Get a specific entry
curl https://trail.services.kibotu.net/api/entries/abc123
# View user's entries
curl https://trail.services.kibotu.net/api/users/alice/entries
# Search user's entries
curl https://trail.services.kibotu.net/api/users/alice/entries?q=example
Authenticated API Usage
To create or modify content, follow these steps:
Step 1: Get Your API Token
- Sign in to Trail at https://trail.services.kibotu.net using Google OAuth
- Navigate to your Profile page
- Find the API Token section (above Muted Users)
- Click the eye icon to reveal your token
- Click the copy icon to copy it to your clipboard
Step 2: Create Your First Entry
# Replace YOUR_API_TOKEN with your actual token
curl -X POST https://trail.services.kibotu.net/api/entries \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Hello from Trail API! 🚀"}'
Step 3: List Your Entries
# List entries you created
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://trail.services.kibotu.net/api/profile
- Store your API token securely (e.g., environment variables)
- Never commit tokens to version control
- Regenerate your token if it's compromised
- Use the same token across all your applications
Authentication & Security
How to Authenticate
API Token Authentication
All API requests require authentication using your personal API token. Each user has a unique, persistent token that never expires.
Getting Your Token:
- Sign in at https://trail.services.kibotu.net
- Go to your Profile page
- Find the API Token section
- Click the eye icon to reveal your token
- Copy the token to use in your requests
Using Your Token:
# Include your token in the Authorization header
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://trail.services.kibotu.net/api/entries
# Example: Create a new entry
curl -X POST \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Hello from API!"}' \
https://trail.services.kibotu.net/api/entries
Token Properties:
- Format: 64-character hexadecimal string
- Expiration: Never expires (until you regenerate it)
- Regeneration: Available anytime from your profile page
- Security: Treat like a password - never share or commit to version control
- Scope: Full access to all API endpoints (same permissions as your account)
- Store tokens in environment variables, not in code
- Never commit tokens to version control (add to .gitignore)
- Regenerate your token immediately if compromised
- Use HTTPS for all API requests (enforced by server)
- Keep your token private - it provides full account access
Security Model
Rate Limiting
- Authentication: 5 attempts per 5 minutes
- General API: 180 requests per minute
- Hourly Limit: 3000 requests per hour
- Response: HTTP 429 when exceeded
Security Features
- CORS: Configurable cross-origin resource sharing
- CSRF Protection: Token-based CSRF prevention
- Content Sanitization: XSS prevention on all user input
- Bot Protection: User-agent validation and pattern detection
- SQL Injection: Prepared statements for all queries
Authorization Levels
Core Concepts
Entries
Short-form posts (max 280 characters) that can include:
- Text: UTF-8 text with emoji support
- URLs: Automatic preview enrichment via Iframely
- Images: Up to 3 images (20MB each, JPEG/PNG/GIF/WebP/SVG/AVIF)
- Videos: MP4, WebM, MOV (20MB each) with custom player controls
- Timestamps: Custom creation dates for imports
- Hash IDs: Secure, obfuscated entry identifiers
Users
User profiles with Google OAuth authentication:
- Nickname: Unique @username for public profiles
- Avatar: Google photo or Gravatar fallback
- Bio: Optional profile description
- Privacy: Public profile pages and RSS feeds
- Statistics: Entry, comment counts, last activity, and login history
Profile stats object:
{
"stats": {
"entry_count": 42,
"link_count": 18,
"comment_count": 7,
"last_entry_at": "2026-02-09 14:30:00",
"previous_login_at": "2026-02-08 09:15:22"
}
}
- entry_count: Total entries by the user
- link_count: Entries containing a URL preview
- comment_count: Total comments by the user
- last_entry_at: Timestamp of the most recent entry (null if none)
- previous_login_at: Timestamp of the login before the current session (null if first login)
Engagement
Social interaction features:
- Claps: 1-50 claps per user per entry/comment
- Comments: Threaded discussions (max 280 chars)
- Mentions: @username mentions with notifications
- Views: Unique view tracking for entries, comments, and profiles
View Tracking
Analytics and view counting for content:
- Entry views: Track unique views per entry
- Comment views: Track unique views per comment
- Profile views: Track unique profile page views
- Deduplication: 24-hour window for unique view counting
- Anonymous support: Optional fingerprint for anonymous users
Pagination
Cursor-based pagination for infinite scroll:
- Parameters:
?limit=20&before=TIMESTAMP - Response:
has_moreandnext_cursorfields - Default: 20 items per page
- Maximum: 100 items per page (public), 50 (user-specific)
Search
Full-text search with relevance ranking:
- Query:
?q=search+termparameter - Full-text: 4+ characters (relevance ranked)
- LIKE search: 1-3 characters (pattern matching)
- Max length: 200 characters
- Public access: No authentication required
- Filtering: Authenticated users get personalized results (muted users hidden)
Moderation
User-controlled content filtering:
- Muting: Hide all content from specific users
- Reporting: Flag entries/comments for review
- Hiding: Personally hide individual entries
- Admin tools: Full moderation capabilities
API Endpoints
All endpoints organized by user journey and functionality. Use the search box to quickly find specific endpoints.
Public Endpoints
No authentication required
Core User Endpoints
Profile and entry management (requires auth)
Engagement
Claps, comments, and view tracking
Media Upload
Image and video upload (requires auth)
Moderation
Content reporting and user muting (requires auth)
Notifications
Real-time updates (requires auth)
Admin Endpoints
Administrative functions (requires admin)
Advanced Features
The POST /api/entries endpoint supports advanced features for importing content from other platforms (e.g., Twitter/X):
Custom Creation Dates
Import entries with their original timestamps using Twitter date format:
"created_at": "Fri Nov 28 10:54:34 +0000 2025"
Format: Day Mon DD HH:MM:SS ±ZZZZ YYYY (e.g., timezone +0000 for UTC, -0800 for PST)
Inline Media Upload
Upload images or videos directly in the request (base64 encoded):
"media": [{
"data": "base64_encoded_media_data...",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"image_type": "post"
}]
Images: JPEG, PNG, GIF (animated supported), WebP, SVG, AVIF. Max 20MB each.
Videos: MP4, WebM, MOV. Max 20MB each. Custom player with progress bar.
Maximum 3 media items per entry.
Raw Upload Mode ADMIN
Skip image processing for faster imports (requires admin privileges):
"raw_upload": true
When enabled, images are saved without resizing or WebP conversion. Use for trusted sources only. Requires admin privileges.
Initial Claps
Set engagement metrics when importing:
"initial_claps": 25
Valid range: 1-50 (normal mode), 1-100,000 (with raw_upload admin mode). Claps are attributed to the authenticated user (author).
Initial Views ADMIN
Set view counts when importing (requires raw_upload mode):
"initial_views": 1500
Only available with raw_upload: true (admin mode). Sets the initial view count for the entry.
Response Format
Successful entry creation returns:
{
"id": 123,
"created_at": "2025-11-28 10:54:34",
"images": [{
"id": 456,
"url": "/uploads/images/1/1_1738246496_abc.webp",
"width": 1200,
"height": 800,
"file_size": 45678
}],
"clap_count": 25,
"user_clap_count": 25,
"view_count": 1500
}
Full-Text Search
The GET /api/entries and GET /api/users/{nickname}/entries endpoints support full-text search:
# Search is public - no authentication required
curl https://trail.services.kibotu.net/api/entries?q=search+term&limit=20
# Search user's entries
curl https://trail.services.kibotu.net/api/users/alice/entries?q=example&limit=20
- Full-text mode: 4+ characters with relevance ranking
- LIKE mode: 1-3 characters with pattern matching
- Max length: 200 characters
- Public access: No authentication required
- Personalization: Authenticated users get filtered results (muted users hidden)
Cursor-Based Pagination
Efficient pagination for infinite scroll implementations:
// First page
GET /api/entries?limit=20
// Next page (use next_cursor from response)
GET /api/entries?limit=20&before=2025-01-15%2010:30:00
Response format:
{
"entries": [...],
"has_more": true,
"next_cursor": "2025-01-15 10:30:00",
"limit": 20
}
URL Preview Enrichment
Automatic link unfurling using Iframely API:
- Detects URLs in entry text automatically
- Fetches title, description, and thumbnail
- Caches previews to minimize API calls
- Graceful fallback if preview fails
Short URL Resolution
Automatic resolution of shortened URLs to their final destinations:
- Automatic on create: Short URLs (t.co, bit.ly, tinyurl.com, etc.) are resolved when posting new entries
- Better previews: Preview metadata is fetched from the final URL, not the shortener
- Graceful fallback: If resolution fails, the original short URL is preserved
- Admin migration tool: Bulk resolve existing short URLs via the admin dashboard
Supported shorteners:
t.co, bit.ly, tinyurl.com, goo.gl, ow.ly, is.gd, buff.ly,
j.mp, dlvr.it, fb.me, lnkd.in, rebrand.ly, cutt.ly, and 20+ more
Performance & Limits
Rate Limits
Content Limits
| Resource | Limit | Notes |
|---|---|---|
| Entry text | 280 characters | UTF-8, emoji supported |
| Comment text | 280 characters | UTF-8, emoji supported |
| Images per entry | 3 max | Per entry or comment |
| Media size | 20MB max | Per image or video |
| Image formats | JPEG, PNG, GIF, WebP, SVG, AVIF | Static images converted to WebP, animated GIFs preserved |
| Video formats | MP4, WebM, MOV | Stored as-is with custom player |
| Search query | 200 characters | Auto-sanitized |
| Initial claps | 1-50 (normal), 1-100,000 (admin) | For imports only (higher limit with raw_upload) |
| Initial views | 0+ (no limit) | Admin only (requires raw_upload) |
| Claps per action | 1-50 range | Per user per entry |
Pagination Limits
- Default limit: 20 items per page
- Max limit (public): 100 items per page
- Max limit (user-specific): 50 items per page
- Cursor format: ISO 8601 timestamp
Caching
- Image ETags: Browser caching for static images
- No-cache headers: Session-dependent content
- URL previews: Cached in database to reduce API calls
Technical Reference
Response Formats
Entry Response
{
"id": 123,
"text": "Entry text",
"created_at": "2025-02-09 12:00:00",
"user_id": 456,
"user_nickname": "alice",
"clap_count": 10,
"comment_count": 3,
"view_count": 42
}
Comment Response
{
"id": 789,
"text": "Comment text",
"created_at": "2025-02-09 12:30:00",
"user_id": 456,
"user_nickname": "alice",
"clap_count": 5,
"view_count": 18
}
Profile Response (GET /api/profile, GET /api/users/{nickname})
{
"id": 456,
"nickname": "alice",
"name": "Alice",
"bio": "Hello world",
"created_at": "2025-06-15 08:00:00",
"stats": {
"entry_count": 42,
"link_count": 18,
"comment_count": 7,
"last_entry_at": "2026-02-09 14:30:00",
"previous_login_at": "2026-02-08 09:15:22"
}
}
Error Response
{
"error": "Error message",
"code": "ERROR_CODE" // Optional
}
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request successful |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Authentication required or token expired |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error occurred |
Error Codes
| Code | Description |
|---|---|
| AUTH_REQUIRED | Authentication required for this operation |
| INVALID_TOKEN | JWT token is invalid or expired |
| RATE_LIMIT_EXCEEDED | Too many requests, please slow down |
| VALIDATION_ERROR | Request validation failed |
Date Formats
ISO 8601 (Default)
"2025-02-09 12:00:00" // YYYY-MM-DD HH:MM:SS
Twitter Format (For Imports)
"Fri Nov 28 10:54:34 +0000 2025" // Day Mon DD HH:MM:SS ±ZZZZ YYYY
Supported timezones: UTC (+0000), PST (-0800), EST (-0500), etc.
Hash IDs
Entry IDs are obfuscated using hash IDs for security and aesthetics:
- Format: Alphanumeric string (e.g., "abc123xyz")
- Usage: Use in URLs and API calls instead of numeric IDs
- Decoding: Automatically handled by API
- Security: Prevents enumeration attacks