API Reference
Base URL: https://app.ultrv.com/api/v1
Authentication
Every request requires a Bearer token. Get your API key from account settings.
Authorization: Bearer ultrv_your_api_key_here
Content-Type: application/json
Accept: application/json The Accept: application/json header is required — without it, some requests return HTML redirects instead of JSON.
Endpoints
Authentication
Blogs
Posts
?status=publishedPost Images
Statistics
?period=1|7|30|90Pages
All paths are relative to /api/v1. Blogs use slugs, posts and pages use numeric IDs.
Auth
These endpoints do not require a Bearer token. Rate limited to 5 requests per minute.
Login
| Field | Type | Description |
|---|---|---|
| string | Required. Account email address | |
| password | string | Required. Account password |
| device_name | string | Required. Label for the token (max 255) |
| totp_code | string | 6-digit TOTP code, required if 2FA is enabled |
Response
{
"token": "1|abc123...",
"user": { "name": "Jane Doe", "email": "jane@example.com" }
} If 2FA is enabled and no totp_code is provided, returns 403 with "two_factor_required": true.
Register
| Field | Type | Description |
|---|---|---|
| name | string | Required. Display name (max 255) |
| string | Required. Must be unique | |
| password | string | Required. Minimum 8 characters |
| password_confirmation | string | Required. Must match password |
| device_name | string | Required. Label for the token (max 255) |
Response (201)
{
"token": "ultrv_abc123...",
"user": { "name": "Jane Doe", "email": "jane@example.com" }
} Quick Start
List your blogs
curl -H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
https://app.ultrv.com/api/v1/blogs Create a post
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title":"My Post","slug":"my-post","content":"<p>Hello</p>","status":"draft"}' \
https://app.ultrv.com/api/v1/blogs/my-blog/posts Upload a post image
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-F "image=@photo.jpg" \
-F "alt_text=A scenic mountain view" \
https://app.ultrv.com/api/v1/blogs/my-blog/posts/42/images Publish a post
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
https://app.ultrv.com/api/v1/blogs/my-blog/posts/42/publish Blogs
Fields
| Field | Type | Description |
|---|---|---|
| name | string | Blog display name (max 255). Required on create. |
| slug | string | URL slug, lowercase with hyphens, unique. Required on create. |
| author_name | string | Author name shown on blog (max 255). Required on create. |
| description | string | Blog description (max 1000) |
| discoverable | boolean | Appear in the explore directory |
| theme | string | Theme identifier (update only) |
| custom_domain | string | Custom domain (update only) |
| meta_title | string | SEO title (max 255, update only) |
| meta_description | string | SEO description (max 1000, update only) |
| og_image | string | Social sharing image URL (update only) |
| language | string | Language code, e.g. en (update only) |
Response
{
"data": {
"id": 1,
"name": "My Blog",
"slug": "my-blog",
"description": "A blog about things.",
"author_name": "Jane Doe",
"custom_domain": null,
"discoverable": true,
"theme": "default",
"meta_title": null,
"meta_description": null,
"og_image": null,
"language": "en",
"canonical_url": "https://my-blog.ultrv.com",
"created_at": "2026-01-15T08:00:00+00:00",
"updated_at": "2026-02-20T14:30:00+00:00"
}
} Posts
Fields
| Field | Type | Description |
|---|---|---|
| title | string | Post title (max 255). Required on create, optional on update. |
| slug | string | URL slug, lowercase with hyphens, unique per blog. Required on create, optional on update. |
| content | string | Post body as HTML (sanitized server-side) |
| excerpt | string | Short summary for listings (max 1000) |
| status | string | draft or published |
| tags | string[] | Tag names. Created automatically if new. Replaces all tags on update. |
| featured_image | string | Featured image URL. Cannot be combined with featured_image_file. |
| featured_image_file | file | Upload via multipart/form-data. JPEG, PNG, GIF, WebP (max 10 MB). Cannot be combined with featured_image. |
Response
{
"data": {
"id": 1,
"title": "Getting Started with ULTRV",
"slug": "getting-started",
"excerpt": "A quick guide to setting up your blog.",
"content": "<p>Welcome to ULTRV...</p>",
"featured_image": null,
"status": "draft",
"published_at": null,
"tags": [
{ "id": 1, "name": "tutorial", "slug": "tutorial" }
],
"images": [],
"created_at": "2026-02-26T12:00:00+00:00",
"updated_at": "2026-02-26T12:00:00+00:00"
}
} Post Images
Upload and manage images for a post. Max 20 images per post. Accepted formats: JPEG, PNG, GIF, WebP (max 10 MB). Images are automatically converted to 4 WebP variants.
Upload fields (multipart/form-data)
| Field | Type | Description |
|---|---|---|
| image | file | Required. Image file (JPEG, PNG, GIF, WebP, max 10 MB) |
| alt_text | string | Alt text (max 255) |
Update fields (PATCH, JSON)
| Field | Type | Description |
|---|---|---|
| alt_text | string | Alt text (max 255) |
| sort_order | integer | Sort position |
Response
All image URLs are returned as complete, absolute URLs. The urls object contains 4 WebP variants:
{
"data": {
"id": 1,
"original_name": "photo.jpg",
"alt_text": "A scenic mountain view",
"width": 1920,
"height": 1080,
"size": 2048576,
"urls": {
"original": "https://app.ultrv.com/storage/uploads/posts/42/a1b2c3d4_original.webp",
"large": "https://app.ultrv.com/storage/uploads/posts/42/a1b2c3d4_large.webp",
"medium": "https://app.ultrv.com/storage/uploads/posts/42/a1b2c3d4_medium.webp",
"thumb": "https://app.ultrv.com/storage/uploads/posts/42/a1b2c3d4_thumb.webp"
},
"sort_order": 0,
"created_at": "2026-04-05T19:00:00+00:00"
}
} Pages
Static content pages (About, Contact, etc.) separate from blog posts.
Fields
| Field | Type | Description |
|---|---|---|
| title | string | Page title (max 255). Required on create, optional on update. |
| slug | string | URL slug, unique per blog. Required on create, optional on update. |
| content | string | Page body as HTML (sanitized server-side) |
| status | string | draft or published |
Reorder pages
POST /blogs/{slug}/pages/reorder
{ "pages": [3, 1, 2] } Array of page IDs in the desired order. Each page's sort_order is set to its array position.
Response
{
"data": {
"id": 1,
"title": "About",
"slug": "about",
"content": "<p>Welcome to my blog...</p>",
"status": "published",
"sort_order": 0,
"created_at": "2026-03-01T10:00:00+00:00",
"updated_at": "2026-03-01T10:00:00+00:00"
}
} Statistics
Blog analytics: views, visitors, top posts, devices, countries, and referrers.
Query parameters
| Param | Type | Description |
|---|---|---|
| period | integer | 1 (today, hourly), 7, 30 (default), or 90 days |
Response
{
"data": {
"total_views": 1250,
"unique_visitors": 830,
"active_visitors": 3,
"views_by_day": [
{ "date": "2026-04-01", "count": 42, "visitors": 28 }
],
"top_posts": [
{ "title": "Getting Started", "slug": "getting-started", "views": 150 }
],
"by_device": [
{ "label": "desktop", "count": 800, "pct": 64 }
],
"by_country": [
{ "label": "US", "count": 400, "pct": 32 }
],
"by_referrer": [
{ "host": "google.com", "count": 200 }
]
}
} When period=1, views_by_day returns hourly buckets with ISO timestamps (e.g. 2026-04-09T14:00:00).
Rate Limits
60 requests per minute per API key. Headers included in every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Retry-After: 30 # only when rate limited Errors
Standard HTTP status codes. Validation errors include field-level detail:
{
"message": "The slug has already been taken.",
"errors": { "slug": ["The slug has already been taken."] }
} 401Missing or invalid API key 403Not authorized 404Resource not found 422Validation error 429Rate limit exceeded Pagination
List endpoints return 20 items per page. Navigate with ?page=2.
{
"data": [...],
"links": { "first": "...?page=1", "last": "...?page=5", "prev": null, "next": "...?page=2" },
"meta": { "current_page": 1, "last_page": 5, "per_page": 20, "total": 100 }
}