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.
API_KEY="lt_live_xxx" # from https://lowtato.com/settingscurl 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", ... }] }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 MarketplaceAuthentication
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.
curl https://lowtato-web-production.up.railway.app/api/v1/accounts \
-H "x-api-key: lt_live_xxx"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.
| Operation | Limit | Window |
|---|---|---|
| Read (GET) | 60 requests | 60 seconds |
| Write (POST / PATCH / DELETE) | 30 requests | 60 seconds |
| File Upload | 30 requests | 60 seconds |
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 respBase URL
https://lowtato-web-production.up.railway.appAll 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
/api/healthReturns 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 https://lowtato-web-production.up.railway.app/api/healthimport requests
resp = requests.get("https://lowtato-web-production.up.railway.app/api/health")
data = resp.json()
assert data["status"] == "ok", "API is down"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.
/api/v1/accountsAuthList 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 https://lowtato-web-production.up.railway.app/api/v1/accounts \
-H "x-api-key: lt_live_xxx"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"]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
);/api/v1/accounts/{accountId}/subaccountsAuthList 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/accountsResponse
{
"subaccounts": [
{
"id": "x1y2z3...",
"external_id": "123456789",
"name": "My Facebook Page",
"type": "page"
}
]
}Code examples (curl, Python, JavaScript)
curl https://lowtato-web-production.up.railway.app/api/v1/accounts/ACCOUNT_ID/subaccounts \
-H "x-api-key: lt_live_xxx"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"]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.
/api/v1/accounts/connectAuthConnect 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 -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"
}'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']}")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.
/api/v1/postsAuthCreate 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)
# 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
}'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,
},
)// 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}`);/api/v1/posts/{postId}AuthRetrieve 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/postsResponse
{
"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 https://lowtato-web-production.up.railway.app/api/v1/posts/POST_ID \
-H "x-api-key: lt_live_xxx"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']}")// 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.
/api/v1/posts/{postId}/publishAuthImmediately 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 immediatelyResponse
{
"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 -X POST https://lowtato-web-production.up.railway.app/api/v1/posts/POST_ID/publish \
-H "x-api-key: lt_live_xxx"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']}")// 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.
/api/v1/media/uploadAuthUpload 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)
# 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"
}'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,
},
)// 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.
/api/v1/schedulesAuthList 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 "https://lowtato-web-production.up.railway.app/api/v1/schedules?limit=10" \
-H "x-api-key: lt_live_xxx"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")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();/api/v1/schedules/{scheduleId}AuthGet full details of a single scheduled post.
Parameters
scheduleId(uuid)— Post IDResponse
{
"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 https://lowtato-web-production.up.railway.app/api/v1/schedules/SCHEDULE_ID \
-H "x-api-key: lt_live_xxx"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())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();/api/v1/schedules/{scheduleId}AuthUpdate the content text or scheduled time of a post. Only posts with status "scheduled" can be updated.
Parameters
scheduleId(uuid)— Post IDRequest 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 -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"}'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())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();/api/v1/schedules/{scheduleId}AuthDelete a scheduled post. Only posts with status "scheduled" can be deleted. Returns { deleted: true } on success.
Parameters
scheduleId(uuid)— Post IDResponse
{
"deleted": true
}Code examples (curl, Python, JavaScript)
curl -X DELETE https://lowtato-web-production.up.railway.app/api/v1/schedules/SCHEDULE_ID \
-H "x-api-key: lt_live_xxx"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 Trueconst 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.
/api/v1/schedule/slotsAuthList 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=SaturdayCode examples (curl, Python, JavaScript)
curl https://lowtato-web-production.up.railway.app/api/v1/schedule/slots \
-H "x-api-key: lt_live_xxx"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")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();/api/v1/schedule/slotsAuthCreate 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 -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"]}
]
}'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")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.
/api/v1/sourcesAuthSubmit 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 -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"
}'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 statusconst 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();/api/v1/sources/{sourceId}AuthCheck 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/sourcesResponse
{
"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 https://lowtato-web-production.up.railway.app/api/v1/sources/SOURCE_ID \
-H "x-api-key: lt_live_xxx"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']}")// 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.
/api/v1/marketplaceAuthBrowse 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)
# 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"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']}")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();/api/v1/marketplace/purchaseAuthPurchase 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 -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
}'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"]// 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.
/api/v1/webhooksAuthRegister 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 -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"]
}'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 securelyconst 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/api/v1/webhooksAuthList 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 https://lowtato-web-production.up.railway.app/api/v1/webhooks \
-H "x-api-key: lt_live_xxx"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"]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();/api/v1/webhooksAuthDelete a webhook by ID.
Request Body
{
"id": "wh_abc123..."
}Response
{
"deleted": true
}Code examples (curl, Python, JavaScript)
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..."}'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 Trueconst 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.
| Status | What it means | Agent action |
|---|---|---|
| draft | Created without scheduledTime | Set scheduledTime via PATCH to queue it |
| scheduled | Waiting for scheduled time | Can update or delete. Poll for status change. |
| publishing | Being sent to the platform now | Wait. Poll every 5s for completion. |
| published | Live on the platform | Read public_url for the live link. |
| failed | Publishing failed | Read error_message. Retry or alert. |
Error Handling
All errors return JSON with an error field. Your agent should handle these status codes.
| Code | Meaning | Agent should |
|---|---|---|
| 200 | Success | Parse response body |
| 201 | Created | Parse response body, store returned ID |
| 400 | Bad request | Fix request body and retry |
| 401 | Unauthorized | Check API key is set correctly |
| 404 | Not found | Verify resource ID exists and belongs to your account |
| 429 | Rate limited | Wait retryAfter seconds, then retry |
| 500 | Server error | Wait 5s and retry. If persistent, check /api/health |
{
"error": "Description of what went wrong"
}
// Rate limit errors also include:
{
"error": "Rate limit exceeded",
"retryAfter": 12 // seconds to wait
}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.
# 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.
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.
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.
| Blotato | Lowtato |
|---|---|
| blotato-api-key header | x-api-key header |
| POST /v2/posts | POST /api/v1/posts |
| GET /v2/users/me/accounts | GET /api/v1/accounts |
| GET /v2/posts/:id | GET /api/v1/posts/:postId |
| GET /v2/scheduled-posts | GET /api/v1/schedules |
| PATCH /v2/scheduled-posts/:id | PATCH /api/v1/schedules/:scheduleId |
| DELETE /v2/scheduled-posts/:id | DELETE /api/v1/schedules/:scheduleId |
# 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.