Agent-First API

The Social Media API Built for AI Agents

One API key. Nine platforms. Zero browser required.

Publish to Twitter, Instagram, LinkedIn, TikTok, Facebook, Threads, Bluesky, YouTube, and Pinterest -- all from a single REST API. Designed for autonomous agents, pipelines, and programmatic workflows.

Quick Start for Agents

Go from zero to published post in 3 API calls.

1. Get your API key (from dashboard Settings, or log in)
API_KEY="lt_live_xxx"  # from https://lowtato.com/settings
2. List your connected social accounts
curl https://lowtato-web-production.up.railway.app/api/v1/accounts \
  -H "x-api-key: $API_KEY"

# Response:
# { "accounts": [{ "id": "acc_123", "platform": "twitter", "username": "@mybot", ... }] }
3. Create and schedule a post
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/posts \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "post": {
      "accountId": "acc_123",
      "content": { "text": "Hello world from my AI agent!" },
      "target": { "targetType": "twitter" }
    },
    "scheduledTime": "2026-04-11T09:00:00Z"
  }'

# Response: { "id": "post_456", "status": "scheduled" }
# Omit scheduledTime to save as draft. Use useNextFreeSlot:true to auto-schedule.

That's it. Your post is scheduled and will be published automatically. Poll GET /api/v1/posts/{postId} to check status.

One API key

Single x-api-key header for all endpoints. No OAuth dance required for API access.

Nine platforms

Twitter, Instagram, LinkedIn, TikTok, Facebook, Threads, Bluesky, YouTube, Pinterest.

Agent-native

JSON in, JSON out. Deterministic responses. Built for programmatic use, not browser UIs.

Don't have a social account?

Buy a pre-configured social media account from our marketplace -- ready for agent use with 2FA handled via 2fa.live. Connect it to your Lowtato workspace and start posting immediately.

Browse Marketplace

Authentication

All endpoints except /api/health require an API key.

Pass your API key via the x-api-key header, or as a Bearer token in the Authorization header. Both methods are equivalent.

x-api-key header (recommended for agents)
curl https://lowtato-web-production.up.railway.app/api/v1/accounts \
  -H "x-api-key: lt_live_xxx"
Bearer token (alternative)
curl https://lowtato-web-production.up.railway.app/api/v1/accounts \
  -H "Authorization: Bearer lt_live_xxx"

Store your API key in environment variables or a secrets manager. Never hardcode it in source files or expose it client-side.

Rate Limits

Per-user limits on a fixed 60-second window. 429 responses include a retryAfter field.

OperationLimitWindow
Read (GET)60 requests60 seconds
Write (POST / PATCH / DELETE)30 requests60 seconds
File Upload30 requests60 seconds
Agent retry pattern (Python)
import time, requests

def api_call(method, url, **kwargs):
    """Make an API call with automatic retry on rate limit."""
    resp = requests.request(method, url, **kwargs)
    if resp.status_code == 429:
        wait = resp.json().get("retryAfter", 5)
        time.sleep(wait)
        return requests.request(method, url, **kwargs)
    return resp

Base URL

https://lowtato-web-production.up.railway.app

All API paths in this documentation are relative to this base URL. The OpenAPI spec is available at /openapi.json and the agent-readable summary is at /llms.txt.

Health Check

GET/api/health

Returns the current health status of the API and database connectivity. No authentication required. Use this to verify API availability before running a workflow.

Response

{
  "status": "ok",
  "timestamp": "2026-04-10T12:00:00.000Z",
  "version": "0.1.0",
  "supabase": "connected"
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/health
Python (requests)
import requests

resp = requests.get("https://lowtato-web-production.up.railway.app/api/health")
data = resp.json()
assert data["status"] == "ok", "API is down"
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/health");
const data = await resp.json();
if (data.status !== "ok") throw new Error("API is down");

Accounts

List and inspect connected social media accounts.

GET/api/v1/accountsAuth

List all social media accounts connected to your workspace. Use this to discover account IDs before creating posts.

Response

{
  "accounts": [
    {
      "id": "a1b2c3d4-...",
      "platform": "twitter",
      "username": "johndoe",
      "fullname": "John Doe",
      "profile_image_url": "https://...",
      "is_active": true
    }
  ]
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/accounts \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/accounts",
    headers={"x-api-key": "lt_live_xxx"}
)
accounts = resp.json()["accounts"]

# Get the first active Twitter account
twitter = next(a for a in accounts if a["platform"] == "twitter" and a["is_active"])
account_id = twitter["id"]
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/accounts", {
  headers: { "x-api-key": "lt_live_xxx" },
});
const { accounts } = await resp.json();

// Get the first active Twitter account
const twitter = accounts.find(
  (a: { platform: string; is_active: boolean }) =>
    a.platform === "twitter" && a.is_active
);
GET/api/v1/accounts/{accountId}/subaccountsAuth

List subaccounts (Facebook Pages, LinkedIn company pages, etc.) for a given social account. Some platforms require posting to a subaccount rather than the main account.

Parameters

accountId(uuid)Social account ID from GET /api/v1/accounts

Response

{
  "subaccounts": [
    {
      "id": "x1y2z3...",
      "external_id": "123456789",
      "name": "My Facebook Page",
      "type": "page"
    }
  ]
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/accounts/ACCOUNT_ID/subaccounts \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    f"https://lowtato-web-production.up.railway.app/api/v1/accounts/{account_id}/subaccounts",
    headers={"x-api-key": "lt_live_xxx"}
)
subaccounts = resp.json()["subaccounts"]
JavaScript (fetch)
const resp = await fetch(
  `https://lowtato-web-production.up.railway.app/api/v1/accounts/${accountId}/subaccounts`,
  { headers: { "x-api-key": "lt_live_xxx" } }
);
const { subaccounts } = await resp.json();

Connect Account (API)

Programmatically connect a social account using OAuth tokens. No browser required.

POST/api/v1/accounts/connectAuth

Connect a social media account to your workspace using platform OAuth tokens. The token is validated against the platform API before the account is saved. Supports all 9 platforms.

Request Body

{
  "platform": "twitter",          // Required: one of the 9 supported platforms
  "access_token": "...",          // Required: valid OAuth access token
  "refresh_token": "...",         // Optional: for token refresh
  "platform_user_id": "12345",   // Required: user ID on the platform
  "platform_username": "@mybot"  // Optional: display username
}

Response

{
  "id": "acc_abc123...",
  "platform": "twitter",
  "username": "@mybot",
  "connected": true
}
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/accounts/connect \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "twitter",
    "access_token": "YOUR_TWITTER_OAUTH_TOKEN",
    "platform_user_id": "12345",
    "platform_username": "@mybot"
  }'
Python (requests)
import requests

resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/accounts/connect",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "platform": "twitter",
        "access_token": "YOUR_TWITTER_OAUTH_TOKEN",
        "platform_user_id": "12345",
        "platform_username": "@mybot",
    },
)
account = resp.json()
print(f"Connected: {account['platform']} {account['username']}")
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/accounts/connect", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    platform: "twitter",
    access_token: "YOUR_TWITTER_OAUTH_TOKEN",
    platform_user_id: "12345",
    platform_username: "@mybot",
  }),
});
const account = await resp.json();

Posts

Create posts and check their publish status.

POST/api/v1/postsAuth

Create a new post. Set scheduledTime to schedule it for later, set useNextFreeSlot to auto-schedule into the next available slot, or omit both to save as a draft.

Request Body

{
  "post": {
    "accountId": "a1b2c3d4-...",
    "content": {
      "text": "Hello from my AI agent!",
      "mediaUrls": [],
      "platform": "twitter"
    },
    "target": {
      "targetType": "twitter"
    }
  },
  "scheduledTime": "2026-04-11T09:00:00Z",
  "useNextFreeSlot": false
}

Response

{
  "id": "p1q2r3s4-...",
  "status": "scheduled"   // or "draft" if no scheduledTime
}
Code examples (curl, Python, JavaScript)
curl
# Schedule a post for tomorrow
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/posts \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "post": {
      "accountId": "ACCOUNT_ID",
      "content": { "text": "Hello from my AI agent!" },
      "target": { "targetType": "twitter" }
    },
    "scheduledTime": "2026-04-11T09:00:00Z"
  }'

# Auto-schedule into the next free slot
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/posts \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "post": {
      "accountId": "ACCOUNT_ID",
      "content": { "text": "Auto-scheduled post" },
      "target": { "targetType": "twitter" }
    },
    "useNextFreeSlot": true
  }'
Python (requests)
import requests

# Create a scheduled post
resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/posts",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "post": {
            "accountId": account_id,
            "content": {"text": "Hello from my AI agent!"},
            "target": {"targetType": "twitter"},
        },
        "scheduledTime": "2026-04-11T09:00:00Z",
    },
)
post = resp.json()
print(f"Post {post['id']} is {post['status']}")

# Auto-schedule into next free slot
resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/posts",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "post": {
            "accountId": account_id,
            "content": {"text": "Auto-scheduled!"},
            "target": {"targetType": "twitter"},
        },
        "useNextFreeSlot": True,
    },
)
JavaScript (fetch)
// Create a scheduled post
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/posts", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    post: {
      accountId: "ACCOUNT_ID",
      content: { text: "Hello from my AI agent!" },
      target: { targetType: "twitter" },
    },
    scheduledTime: "2026-04-11T09:00:00Z",
  }),
});
const post = await resp.json();
console.log(`Post ${post.id} is ${post.status}`);
GET/api/v1/posts/{postId}Auth

Retrieve a single post by ID. Use this to poll for publish status after scheduling. Check the status field: 'published' means it's live, 'failed' means check error_message.

Parameters

postId(uuid)Post ID returned from POST /api/v1/posts

Response

{
  "id": "p1q2r3s4-...",
  "status": "published",
  "content_text": "Hello from my AI agent!",
  "target_type": "twitter",
  "social_account_id": "a1b2c3d4-...",
  "media_urls": [],
  "public_url": "https://x.com/johndoe/status/123456",
  "error_message": null,
  "scheduled_time": "2026-04-11T09:00:00Z",
  "published_at": "2026-04-11T09:00:05Z"
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/posts/POST_ID \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests, time

# Poll until post is published or failed
post_id = "POST_ID"
while True:
    resp = requests.get(
        f"https://lowtato-web-production.up.railway.app/api/v1/posts/{post_id}",
        headers={"x-api-key": "lt_live_xxx"}
    )
    post = resp.json()
    if post["status"] in ("published", "failed"):
        break
    time.sleep(5)

if post["status"] == "published":
    print(f"Live at: {post['public_url']}")
else:
    print(f"Failed: {post['error_message']}")
JavaScript (fetch)
// Poll until published
const checkPost = async (postId: string) => {
  while (true) {
    const resp = await fetch(`https://lowtato-web-production.up.railway.app/api/v1/posts/${postId}`, {
      headers: { "x-api-key": "lt_live_xxx" },
    });
    const post = await resp.json();
    if (post.status === "published" || post.status === "failed") {
      return post;
    }
    await new Promise((r) => setTimeout(r, 5000));
  }
};

Publish Now

Immediately publish an existing post without waiting for a schedule.

POST/api/v1/posts/{postId}/publishAuth

Immediately publish a draft or scheduled post. The post is sent to the platform synchronously and the response includes the live public_url on success. Returns 409 if the post is already published or currently publishing.

Parameters

postId(uuid)Post ID to publish immediately

Response

{
  "status": "published",
  "public_url": "https://x.com/johndoe/status/123456"
}

// If already published (409):
{ "error": "Post is already published", "public_url": "https://..." }
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/posts/POST_ID/publish \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

# Create a draft, then publish immediately
post = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/posts",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "post": {
            "accountId": account_id,
            "content": {"text": "Publishing right now!"},
            "target": {"targetType": "twitter"},
        }
    },
).json()

# Publish it immediately
result = requests.post(
    f"https://lowtato-web-production.up.railway.app/api/v1/posts/{post['id']}/publish",
    headers={"x-api-key": "lt_live_xxx"},
).json()

print(f"Live at: {result['public_url']}")
JavaScript (fetch)
// Create then immediately publish
const post = await fetch("https://lowtato-web-production.up.railway.app/api/v1/posts", {
  method: "POST",
  headers: { "x-api-key": "lt_live_xxx", "Content-Type": "application/json" },
  body: JSON.stringify({
    post: {
      accountId: "ACCOUNT_ID",
      content: { text: "Publishing right now!" },
      target: { targetType: "twitter" },
    },
  }),
}).then((r) => r.json());

const result = await fetch(
  `https://lowtato-web-production.up.railway.app/api/v1/posts/${post.id}/publish`,
  { method: "POST", headers: { "x-api-key": "lt_live_xxx" } }
).then((r) => r.json());

Media Upload

Upload images and videos for use in posts. Supports multipart form data and base64 JSON.

POST/api/v1/media/uploadAuth

Upload an image or video file. Accepts multipart/form-data (field: 'file') or JSON with base64 data. Returns a URL to use in mediaUrls when creating posts. Supports JPEG, PNG, GIF, WebP, MP4, WebM.

Request Body

// Option 1: Multipart form data
Content-Type: multipart/form-data
field "file": <binary>

// Option 2: JSON with base64
{
  "data": "base64_encoded_string...",
  "mime_type": "image/png",
  "filename": "chart.png"
}

Response

{
  "id": "asset_abc123...",
  "url": "https://r2.lowtato.com/uploads/...",
  "mime_type": "image/png",
  "size": 45678
}
Code examples (curl, Python, JavaScript)
curl
# Multipart upload
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/media/upload \
  -H "x-api-key: lt_live_xxx" \
  -F "file=@./screenshot.png"

# Base64 JSON upload
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/media/upload \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "data": "'$(base64 -i ./screenshot.png)'",
    "mime_type": "image/png",
    "filename": "screenshot.png"
  }'
Python (requests)
import requests, base64

# Multipart upload
with open("screenshot.png", "rb") as f:
    resp = requests.post(
        "https://lowtato-web-production.up.railway.app/api/v1/media/upload",
        headers={"x-api-key": "lt_live_xxx"},
        files={"file": ("screenshot.png", f, "image/png")},
    )
media_url = resp.json()["url"]

# Then use in a post
requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/posts",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "post": {
            "accountId": account_id,
            "content": {
                "text": "Check out this chart!",
                "mediaUrls": [media_url],
            },
            "target": {"targetType": "twitter"},
        },
        "useNextFreeSlot": True,
    },
)
JavaScript (fetch)
// Upload with fetch + FormData
const formData = new FormData();
formData.append("file", fileBlob, "screenshot.png");

const upload = await fetch("https://lowtato-web-production.up.railway.app/api/v1/media/upload", {
  method: "POST",
  headers: { "x-api-key": "lt_live_xxx" },
  body: formData,
});
const { url: mediaUrl } = await upload.json();

// Use in a post
await fetch("https://lowtato-web-production.up.railway.app/api/v1/posts", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    post: {
      accountId: "ACCOUNT_ID",
      content: { text: "Check out this chart!", mediaUrls: [mediaUrl] },
      target: { targetType: "twitter" },
    },
    useNextFreeSlot: true,
  }),
});

Schedules

View, update, and delete scheduled posts.

GET/api/v1/schedulesAuth

List all posts with status "scheduled", ordered by scheduled time ascending. Use this to see your queue.

Parameters

limit(integer)Max posts to return (default: 20, max: 100)

Response

{
  "scheduledPosts": [
    {
      "id": "p1q2r3s4-...",
      "content_text": "Hello from the API!",
      "target_type": "twitter",
      "scheduled_time": "2026-04-11T09:00:00Z",
      "status": "scheduled"
    }
  ]
}
Code examples (curl, Python, JavaScript)
curl
curl "https://lowtato-web-production.up.railway.app/api/v1/schedules?limit=10" \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/schedules",
    headers={"x-api-key": "lt_live_xxx"},
    params={"limit": 10}
)
queue = resp.json()["scheduledPosts"]
print(f"{len(queue)} posts in queue")
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/schedules?limit=10", {
  headers: { "x-api-key": "lt_live_xxx" },
});
const { scheduledPosts } = await resp.json();
GET/api/v1/schedules/{scheduleId}Auth

Get full details of a single scheduled post.

Parameters

scheduleId(uuid)Post ID

Response

{
  "id": "p1q2r3s4-...",
  "content_text": "Hello from the API!",
  "target_type": "twitter",
  "social_account_id": "a1b2c3d4-...",
  "media_urls": [],
  "target_options": {},
  "status": "scheduled",
  "scheduled_time": "2026-04-11T09:00:00Z"
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/schedules/SCHEDULE_ID \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    f"https://lowtato-web-production.up.railway.app/api/v1/schedules/{schedule_id}",
    headers={"x-api-key": "lt_live_xxx"}
)
print(resp.json())
JavaScript (fetch)
const resp = await fetch(`https://lowtato-web-production.up.railway.app/api/v1/schedules/${scheduleId}`, {
  headers: { "x-api-key": "lt_live_xxx" },
});
const data = await resp.json();
PATCH/api/v1/schedules/{scheduleId}Auth

Update the content text or scheduled time of a post. Only posts with status "scheduled" can be updated.

Parameters

scheduleId(uuid)Post ID

Request Body

{
  "content_text": "Updated content!",
  "scheduled_time": "2026-04-12T14:00:00Z"
}

Response

{
  "id": "p1q2r3s4-...",
  "content_text": "Updated content!",
  "scheduled_time": "2026-04-12T14:00:00Z",
  "status": "scheduled"
}
Code examples (curl, Python, JavaScript)
curl
curl -X PATCH https://lowtato-web-production.up.railway.app/api/v1/schedules/SCHEDULE_ID \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"content_text": "Updated content!", "scheduled_time": "2026-04-12T14:00:00Z"}'
Python (requests)
import requests

resp = requests.patch(
    f"https://lowtato-web-production.up.railway.app/api/v1/schedules/{schedule_id}",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "content_text": "Updated content!",
        "scheduled_time": "2026-04-12T14:00:00Z",
    },
)
print(resp.json())
JavaScript (fetch)
const resp = await fetch(`https://lowtato-web-production.up.railway.app/api/v1/schedules/${scheduleId}`, {
  method: "PATCH",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content_text: "Updated content!",
    scheduled_time: "2026-04-12T14:00:00Z",
  }),
});
const data = await resp.json();
DELETE/api/v1/schedules/{scheduleId}Auth

Delete a scheduled post. Only posts with status "scheduled" can be deleted. Returns { deleted: true } on success.

Parameters

scheduleId(uuid)Post ID

Response

{
  "deleted": true
}
Code examples (curl, Python, JavaScript)
curl
curl -X DELETE https://lowtato-web-production.up.railway.app/api/v1/schedules/SCHEDULE_ID \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.delete(
    f"https://lowtato-web-production.up.railway.app/api/v1/schedules/{schedule_id}",
    headers={"x-api-key": "lt_live_xxx"}
)
assert resp.json()["deleted"] is True
JavaScript (fetch)
const resp = await fetch(`https://lowtato-web-production.up.railway.app/api/v1/schedules/${scheduleId}`, {
  method: "DELETE",
  headers: { "x-api-key": "lt_live_xxx" },
});
const data = await resp.json();

Schedule Slots

Configure recurring time slots for auto-scheduling. When you create a post with useNextFreeSlot: true, it fills the next available slot.

GET/api/v1/schedule/slotsAuth

List all configured schedule slots for your account.

Response

{
  "slots": [
    {
      "id": "s1t2u3-...",
      "day": 1,
      "hour": 9,
      "minute": 0,
      "selected_targets": ["a1b2c3d4-..."]
    }
  ]
}
// day: 0=Sunday, 1=Monday ... 6=Saturday
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/schedule/slots \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/schedule/slots",
    headers={"x-api-key": "lt_live_xxx"}
)
slots = resp.json()["slots"]
print(f"{len(slots)} recurring slots configured")
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/schedule/slots", {
  headers: { "x-api-key": "lt_live_xxx" },
});
const { slots } = await resp.json();
POST/api/v1/schedule/slotsAuth

Create one or more recurring schedule slots. Day 0 is Sunday, day 6 is Saturday. Assign target account IDs to each slot.

Request Body

{
  "slots": [
    {
      "day": 1,
      "hour": 9,
      "minute": 0,
      "selectedTargets": ["ACCOUNT_ID"]
    },
    {
      "day": 3,
      "hour": 14,
      "minute": 30,
      "selectedTargets": ["ACCOUNT_ID"]
    }
  ]
}

Response

{
  "created": 2
}
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/schedule/slots \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "slots": [
      {"day": 1, "hour": 9, "minute": 0, "selectedTargets": ["ACCOUNT_ID"]},
      {"day": 3, "hour": 14, "minute": 30, "selectedTargets": ["ACCOUNT_ID"]}
    ]
  }'
Python (requests)
import requests

resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/schedule/slots",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "slots": [
            {"day": 1, "hour": 9, "minute": 0, "selectedTargets": [account_id]},
            {"day": 3, "hour": 14, "minute": 30, "selectedTargets": [account_id]},
        ]
    },
)
print(f"Created {resp.json()['created']} slots")
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/schedule/slots", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    slots: [
      { day: 1, hour: 9, minute: 0, selectedTargets: [accountId] },
      { day: 3, hour: 14, minute: 30, selectedTargets: [accountId] },
    ],
  }),
});
const data = await resp.json();

Sources

Submit URLs or raw text for AI-powered content extraction. Useful for generating post content from articles, blog posts, or notes.

POST/api/v1/sourcesAuth

Submit a URL or text for AI content extraction. Processing starts immediately in the background. Poll the source status to check when extraction is complete.

Request Body

{
  "sourceType": "url",           // "url" or "text"
  "url": "https://example.com/blog-post",
  "customInstructions": "Extract key points for a Twitter thread"
}

// Or for raw text:
{
  "sourceType": "text",
  "text": "Your raw content here...",
  "customInstructions": "Summarize into 3 tweet-sized bullets"
}

Response

{
  "id": "src1-...",
  "status": "queued"
}
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/sources \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceType": "url",
    "url": "https://example.com/blog-post",
    "customInstructions": "Extract key points for a Twitter thread"
  }'
Python (requests)
import requests

resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/sources",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "sourceType": "url",
        "url": "https://example.com/blog-post",
        "customInstructions": "Extract key points for a Twitter thread",
    },
)
source = resp.json()
source_id = source["id"]  # Use to poll status
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/sources", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    sourceType: "url",
    url: "https://example.com/blog-post",
    customInstructions: "Extract key points for a Twitter thread",
  }),
});
const source = await resp.json();
GET/api/v1/sources/{sourceId}Auth

Check processing status and retrieve extracted content. Status progresses: queued -> processing -> ready (or failed). Once status is "ready", extracted_title and extracted_content are populated.

Parameters

sourceId(uuid)Source ID from POST /api/v1/sources

Response

{
  "id": "src1-...",
  "source_type": "url",
  "status": "ready",
  "extracted_title": "10 Tips for Social Media Growth",
  "extracted_content": "Here are the key insights...",
  "error_message": null
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/sources/SOURCE_ID \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests, time

# Poll until processing is complete
while True:
    resp = requests.get(
        f"https://lowtato-web-production.up.railway.app/api/v1/sources/{source_id}",
        headers={"x-api-key": "lt_live_xxx"}
    )
    source = resp.json()
    if source["status"] in ("ready", "failed"):
        break
    time.sleep(3)

if source["status"] == "ready":
    print(source["extracted_content"])
else:
    print(f"Extraction failed: {source['error_message']}")
JavaScript (fetch)
// Poll until ready
const pollSource = async (sourceId: string) => {
  while (true) {
    const resp = await fetch(
      `https://lowtato-web-production.up.railway.app/api/v1/sources/${sourceId}`,
      { headers: { "x-api-key": "lt_live_xxx" } }
    );
    const source = await resp.json();
    if (source.status === "ready" || source.status === "failed") {
      return source;
    }
    await new Promise((r) => setTimeout(r, 3000));
  }
};

Marketplace

Browse and purchase pre-configured social media accounts. Ready for agent use.

GET/api/v1/marketplaceAuth

Browse available social media accounts for purchase. Filter by platform. Account usernames are partially masked until purchase.

Parameters

platform(string)Optional filter: twitter, instagram, tiktok, etc.

Response

{
  "accounts": [
    {
      "id": "inv_abc123...",
      "platform": "twitter",
      "username_masked": "joh***oe",
      "price_cents": 2500,
      "price_display": "$25.00",
      "metadata": {}
    }
  ],
  "count": 1
}
Code examples (curl, Python, JavaScript)
curl
# List all available accounts
curl "https://lowtato-web-production.up.railway.app/api/v1/marketplace" \
  -H "x-api-key: lt_live_xxx"

# Filter by platform
curl "https://lowtato-web-production.up.railway.app/api/v1/marketplace?platform=twitter" \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/marketplace",
    headers={"x-api-key": "lt_live_xxx"},
    params={"platform": "twitter"},
)
accounts = resp.json()["accounts"]
for a in accounts:
    print(f"{a['platform']} {a['username_masked']} - {a['price_display']}")
JavaScript (fetch)
const resp = await fetch(
  "https://lowtato-web-production.up.railway.app/api/v1/marketplace?platform=twitter",
  { headers: { "x-api-key": "lt_live_xxx" } }
);
const { accounts } = await resp.json();
POST/api/v1/marketplace/purchaseAuth

Purchase a social media account. Pays with credits if available, otherwise charges your card on file. Set auto_connect: true to automatically connect the account to your workspace.

Request Body

{
  "account_id": "inv_abc123...",   // Required: from GET /api/v1/marketplace
  "auto_connect": true             // Optional: auto-connect to your workspace
}

Response

{
  "account": {
    "id": "inv_abc123...",
    "platform": "twitter",
    "username": "johndoe",
    "email": "johndoe@example.com",
    "password": "...",
    "totp_secret": "..."
  },
  "paid_with": "credits",
  "connected": true,
  "social_account_id": "acc_xyz789..."
}
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/marketplace/purchase \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "inv_abc123...",
    "auto_connect": true
  }'
Python (requests)
import requests

# Browse, pick, purchase, and start posting
accounts = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/marketplace?platform=twitter",
    headers={"x-api-key": "lt_live_xxx"},
).json()["accounts"]

if accounts:
    purchase = requests.post(
        "https://lowtato-web-production.up.railway.app/api/v1/marketplace/purchase",
        headers={"x-api-key": "lt_live_xxx"},
        json={
            "account_id": accounts[0]["id"],
            "auto_connect": True,
        },
    ).json()

    if purchase.get("connected"):
        # Ready to post!
        account_id = purchase["social_account_id"]
JavaScript (fetch)
// Full flow: browse -> purchase -> post
const { accounts } = await fetch(
  "https://lowtato-web-production.up.railway.app/api/v1/marketplace?platform=twitter",
  { headers: { "x-api-key": "lt_live_xxx" } }
).then((r) => r.json());

if (accounts.length > 0) {
  const purchase = await fetch("https://lowtato-web-production.up.railway.app/api/v1/marketplace/purchase", {
    method: "POST",
    headers: {
      "x-api-key": "lt_live_xxx",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account_id: accounts[0].id,
      auto_connect: true,
    }),
  }).then((r) => r.json());
}

Webhooks

Preview — coming soon. You can register webhook endpoints today, but events are not delivered yet. Until delivery ships, poll the posts API for status changes.

Not yet available: webhook delivery and payload signing are still in development. Registering a webhook stores your endpoint and returns a secret, but no events are sent to it yet. Continue polling the posts API for status until this section is marked generally available.
POST/api/v1/webhooksAuth

Register a webhook URL for future event notifications (delivery not yet live). URL must use HTTPS. Returns a signing secret you will use to verify payloads once delivery ships.

Request Body

{
  "url": "https://your-agent.com/webhooks/lowtato",
  "events": ["post.published", "post.failed", "post.scheduled"]
}

Response

{
  "id": "wh_abc123...",
  "url": "https://your-agent.com/webhooks/lowtato",
  "events": ["post.published", "post.failed", "post.scheduled"],
  "active": true,
  "secret": "whsec_...",
  "created_at": "2026-04-10T12:00:00.000Z"
}
Code examples (curl, Python, JavaScript)
curl
curl -X POST https://lowtato-web-production.up.railway.app/api/v1/webhooks \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-agent.com/webhooks/lowtato",
    "events": ["post.published", "post.failed"]
  }'
Python (requests)
import requests

resp = requests.post(
    "https://lowtato-web-production.up.railway.app/api/v1/webhooks",
    headers={"x-api-key": "lt_live_xxx"},
    json={
        "url": "https://your-agent.com/webhooks/lowtato",
        "events": ["post.published", "post.failed"],
    },
)
webhook = resp.json()
print(f"Webhook secret: {webhook['secret']}")  # Store this securely
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/webhooks", {
  method: "POST",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-agent.com/webhooks/lowtato",
    events: ["post.published", "post.failed"],
  }),
});
const webhook = await resp.json();
// Store webhook.secret for payload verification
GET/api/v1/webhooksAuth

List all registered webhooks for your account.

Response

{
  "webhooks": [
    {
      "id": "wh_abc123...",
      "url": "https://your-agent.com/webhooks/lowtato",
      "events": ["post.published", "post.failed"],
      "is_active": true,
      "created_at": "2026-04-10T12:00:00.000Z"
    }
  ]
}
Code examples (curl, Python, JavaScript)
curl
curl https://lowtato-web-production.up.railway.app/api/v1/webhooks \
  -H "x-api-key: lt_live_xxx"
Python (requests)
import requests

resp = requests.get(
    "https://lowtato-web-production.up.railway.app/api/v1/webhooks",
    headers={"x-api-key": "lt_live_xxx"},
)
webhooks = resp.json()["webhooks"]
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/webhooks", {
  headers: { "x-api-key": "lt_live_xxx" },
});
const { webhooks } = await resp.json();
DELETE/api/v1/webhooksAuth

Delete a webhook by ID.

Request Body

{
  "id": "wh_abc123..."
}

Response

{
  "deleted": true
}
Code examples (curl, Python, JavaScript)
curl
curl -X DELETE https://lowtato-web-production.up.railway.app/api/v1/webhooks \
  -H "x-api-key: lt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": "wh_abc123..."}'
Python (requests)
import requests

resp = requests.delete(
    "https://lowtato-web-production.up.railway.app/api/v1/webhooks",
    headers={"x-api-key": "lt_live_xxx"},
    json={"id": "wh_abc123..."},
)
assert resp.json()["deleted"] is True
JavaScript (fetch)
const resp = await fetch("https://lowtato-web-production.up.railway.app/api/v1/webhooks", {
  method: "DELETE",
  headers: {
    "x-api-key": "lt_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: "wh_abc123..." }),
});

Post Status Lifecycle

Posts progress through these statuses. Your agent should handle each state appropriately.

draft
scheduled
publishing
published
failed
StatusWhat it meansAgent action
draftCreated without scheduledTimeSet scheduledTime via PATCH to queue it
scheduledWaiting for scheduled timeCan update or delete. Poll for status change.
publishingBeing sent to the platform nowWait. Poll every 5s for completion.
publishedLive on the platformRead public_url for the live link.
failedPublishing failedRead error_message. Retry or alert.

Error Handling

All errors return JSON with an error field. Your agent should handle these status codes.

CodeMeaningAgent should
200SuccessParse response body
201CreatedParse response body, store returned ID
400Bad requestFix request body and retry
401UnauthorizedCheck API key is set correctly
404Not foundVerify resource ID exists and belongs to your account
429Rate limitedWait retryAfter seconds, then retry
500Server errorWait 5s and retry. If persistent, check /api/health
Error response format
{
  "error": "Description of what went wrong"
}

// Rate limit errors also include:
{
  "error": "Rate limit exceeded",
  "retryAfter": 12   // seconds to wait
}
Robust error handling (Python)
import requests, time

def lowtato_request(method, path, **kwargs):
    """Make API call with retry logic for agents."""
    url = f"https://lowtato-web-production.up.railway.app{path}"
    headers = {"x-api-key": "lt_live_xxx", **kwargs.pop("headers", {})}

    for attempt in range(3):
        resp = requests.request(method, url, headers=headers, **kwargs)

        if resp.status_code == 429:
            wait = resp.json().get("retryAfter", 5)
            time.sleep(wait)
            continue

        if resp.status_code >= 500:
            time.sleep(5)
            continue

        resp.raise_for_status()
        return resp.json()

    raise Exception(f"API call failed after 3 retries: {method} {path}")

Agent Integration Guide

How to use Lowtato with popular AI agent frameworks.

Claude Code / OpenClaw

Claude Code can call the Lowtato API directly using bash commands or by reading the OpenAPI spec. Point it at /llms.txt or /openapi.json for the full API surface.

In your CLAUDE.md or system prompt
# Social Media API
Lowtato API is available for social media posting.
Base URL: https://lowtato-web-production.up.railway.app
API Key: $LOWTATO_API_KEY (set in environment)
Docs: https://lowtato.com/llms.txt
OpenAPI: https://lowtato-web-production.up.railway.app/openapi.json

# Usage
- List accounts: GET /api/v1/accounts
- Create post: POST /api/v1/posts
- Check status: GET /api/v1/posts/{id}

LangChain / CrewAI

Use the OpenAPI spec to auto-generate tool definitions, or wrap the API calls as custom tools.

LangChain custom tool (Python)
from langchain.tools import tool
import requests

API_KEY = "lt_live_xxx"
BASE = "https://lowtato-web-production.up.railway.app"

@tool
def post_to_social_media(account_id: str, text: str, platform: str) -> str:
    """Publish a post to social media via Lowtato."""
    resp = requests.post(
        f"{BASE}/api/v1/posts",
        headers={"x-api-key": API_KEY},
        json={
            "post": {
                "accountId": account_id,
                "content": {"text": text},
                "target": {"targetType": platform},
            },
            "useNextFreeSlot": True,
        },
    )
    data = resp.json()
    return f"Post created: {data['id']} (status: {data['status']})"

@tool
def list_social_accounts() -> str:
    """List connected social media accounts."""
    resp = requests.get(
        f"{BASE}/api/v1/accounts",
        headers={"x-api-key": API_KEY},
    )
    accounts = resp.json()["accounts"]
    return "\n".join(
        f"{a['platform']}: {a['username']} (id: {a['id']})"
        for a in accounts if a["is_active"]
    )

Custom Python Agent

A minimal Lowtato client class for use in any Python agent.

lowtato_client.py
import requests, time

class LowtaroClient:
    def __init__(self, api_key: str):
        self.base = "https://lowtato-web-production.up.railway.app"
        self.headers = {"x-api-key": api_key}

    def _req(self, method, path, **kw):
        resp = requests.request(
            method, f"{self.base}{path}",
            headers=self.headers, **kw
        )
        if resp.status_code == 429:
            time.sleep(resp.json().get("retryAfter", 5))
            return self._req(method, path, **kw)
        resp.raise_for_status()
        return resp.json()

    def accounts(self):
        return self._req("GET", "/api/v1/accounts")["accounts"]

    def create_post(self, account_id, text, platform, schedule=None):
        body = {
            "post": {
                "accountId": account_id,
                "content": {"text": text},
                "target": {"targetType": platform},
            }
        }
        if schedule:
            body["scheduledTime"] = schedule
        else:
            body["useNextFreeSlot"] = True
        return self._req("POST", "/api/v1/posts", json=body)

    def get_post(self, post_id):
        return self._req("GET", f"/api/v1/posts/{post_id}")

    def wait_for_publish(self, post_id, timeout=120):
        start = time.time()
        while time.time() - start < timeout:
            post = self.get_post(post_id)
            if post["status"] in ("published", "failed"):
                return post
            time.sleep(5)
        raise TimeoutError("Post did not publish within timeout")

# Usage:
# client = LowtaroClient("lt_live_xxx")
# accounts = client.accounts()
# post = client.create_post(accounts[0]["id"], "Hello!", "twitter")
# result = client.wait_for_publish(post["id"])

Best Practices for Agents

  • Always check /api/health first before running a multi-step workflow.
  • Cache account IDs-- they don't change frequently. Fetch once per session.
  • Use useNextFreeSlot: true instead of calculating schedule times manually.
  • Poll with backoff -- check post status every 5 seconds, not continuously.
  • Handle 429s gracefully -- read the retryAfter field and wait.
  • Store the post ID from create responses -- you need it to check publish status.
  • Validate content length before posting. Twitter: 280 chars. LinkedIn: 3000 chars. Threads: 500 chars.
  • Read /llms.txt for a machine-readable summary of all endpoints.

Blotato Migration

Switching from Blotato? Here's the mapping. Migrate in 5 minutes.

BlotatoLowtato
blotato-api-key headerx-api-key header
POST /v2/postsPOST /api/v1/posts
GET /v2/users/me/accountsGET /api/v1/accounts
GET /v2/posts/:idGET /api/v1/posts/:postId
GET /v2/scheduled-postsGET /api/v1/schedules
PATCH /v2/scheduled-posts/:idPATCH /api/v1/schedules/:scheduleId
DELETE /v2/scheduled-posts/:idDELETE /api/v1/schedules/:scheduleId
Migration diff (Python)
# Before (Blotato):
- headers = {"blotato-api-key": BLOTATO_KEY}
- resp = requests.post("https://api.blotato.com/v2/posts", ...)

# After (Lowtato):
+ headers = {"x-api-key": LOWTATO_KEY}
+ resp = requests.post("https://lowtato-web-production.up.railway.app/api/v1/posts", ...)

Same features, 33% cheaper.

Lowtato supports all the platforms Blotato does, at significantly lower prices. The API surface is nearly identical -- most agents can switch by changing the base URL and header name.