ULTRVA

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

POST/loginreturns API token
POST/registercreate account + API key

Blogs

GET/blogs
POST/blogs
GET/blogs/{slug}
PUT/blogs/{slug}
DELETE/blogs/{slug}

Posts

GET/blogs/{slug}/postsfilter: ?status=published
POST/blogs/{slug}/posts
GET/blogs/{slug}/posts/{id}
PUT/blogs/{slug}/posts/{id}
DELETE/blogs/{slug}/posts/{id}
POST/blogs/{slug}/posts/{id}/publish
POST/blogs/{slug}/posts/{id}/unpublish

Post Images

GET/blogs/{slug}/posts/{id}/images
POST/blogs/{slug}/posts/{id}/imagesmultipart/form-data
PATCH/blogs/{slug}/posts/{id}/images/{imageId}
DELETE/blogs/{slug}/posts/{id}/images/{imageId}

Statistics

GET/blogs/{slug}/statisticsfilter: ?period=1|7|30|90

Pages

GET/blogs/{slug}/pages
POST/blogs/{slug}/pages
GET/blogs/{slug}/pages/{id}
PUT/blogs/{slug}/pages/{id}
DELETE/blogs/{slug}/pages/{id}
POST/blogs/{slug}/pages/{id}/publish
POST/blogs/{slug}/pages/{id}/unpublish
POST/blogs/{slug}/pages/reorder

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
email 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)
email 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 }
}