Frekto API
Generate AI-powered social media posts — images and animated videos — via a simple REST API.
How it works
Frekto uses a two-step async pattern — submit a job, then poll for the result.
# 1. Submit a job — returns immediately
POST /generate → { job_id: "job_123" }
# 2. Poll until done — usually 15–90 seconds
GET /jobs/job_123 → { status: "done", output_url: "https://..." }
Authentication
Pass your API key in the Authorization header on every request.
Authorization: Bearer frekto_live_YOUR_KEY
Rate Limits
When the limit is reached the API returns 429 Too Many Requests. Limits reset at midnight UTC.
POST /generate
Create a render job. Returns a job_id immediately — rendering happens asynchronously.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| topic | string | required | Topic or subject for the post. Max 300 characters. |
| style | string | optional | Template style key e.g. revelation::cinematic. Omit or pass auto for AI to pick the best fit based on your topic. See styles. |
| format | string | optional | 4:5 (default), 9:16, 1:1 |
| output_format | string | optional | png (default) or mp4 |
| asset_url | string | optional | Public URL of background image/video. Must allow CORS. |
| callback_url | string | optional | Webhook URL to notify on completion |
Example request
{
"topic": "AI is transforming healthcare",
"style": "revelation::cinematic",
"format": "4:5",
"output_format": "mp4"
}
Response
{
"job_id": "job_1782400942804_abc123",
"status": "queued"
}
Error responses
| Status | Error | Cause |
|---|---|---|
| 401 | Invalid or missing API key | Missing or wrong Authorization header |
| 400 | topic is required | Missing or empty topic |
| 400 | topic must be 300 characters or less | Topic too long |
| 400 | format must be one of: 4:5, 9:16, 1:1 | Invalid format value |
| 400 | output_format must be one of: mp4, png | Invalid output format |
| 400 | Unknown style "..." | Style key not in the valid list |
| 400 | style "media::..." requires asset_url | Media styles need a public image/video URL |
| 400 | Request body must be valid JSON | Malformed request body |
| 422 | No linkedin account connected. Connect it at app.frekto.ai/settings... | No social account — connect one first |
Before any render starts, the API checks that you have a social account connected. If not, you receive a 422 immediately — no job is created, no credits used. Connect accounts at Settings → Social Accounts.
GET /jobs/:id
Poll a render job by ID. Call every 3–5 seconds until status is done or failed.
Status values
queued rendering done failed
Response — done
{
"id": "job_1782400942804_abc123",
"status": "done",
"output_url": "https://pub-xxx.r2.dev/renders/job_abc123.png",
"error": null,
"created_at": 1782400942804,
"completed_at": 1782400978311
}
POST /generate/series
Generate a full content series from a single instruction. The worker plans N posts, renders each one on Railway, then auto-schedules them — all from one API call. Returns a series_id immediately.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| instruction | string | required | What the series should be about. Max 500 characters. |
| count | number | optional | Number of posts to generate (2–10, default 3) |
| platform | string | optional | linkedin (default), instagram, twitter |
| cadence | string | optional | Post spacing: weekdays (default), daily, mwf, linkedin |
| format | string | optional | 4:5 (default), 9:16, 1:1 |
| output_format | string | optional | mp4 (default) or png |
| schedule.start_date | string | optional | ISO date string for first post. Defaults to tomorrow. |
| callback_url | string | optional | Webhook called when all posts are rendered and scheduled |
Example request
{
"instruction": "5 posts on building a personal brand as a developer",
"count": 3,
"platform": "linkedin",
"cadence": "weekdays",
"output_format": "mp4",
"schedule": { "start_date": "2026-07-08" },
"callback_url": "https://yoursite.com/webhook/series"
}
Response
{
"series_id": "ser_1783200000000_abc12",
"status": "rendering",
"total": 3,
"series_title": "Building a Personal Brand as a Developer"
}
Poll GET /series/:id every 5 seconds to track progress. Each post renders in parallel on Railway — a 3-post series typically completes in 60–90 seconds.
GET /series/:id
Poll a series by ID. Posts appear with URLs as each render completes. When all posts are done the worker automatically inserts them into your scheduled queue.
Status values
planning rendering done scheduled partial failed
Response — rendering
{
"series_id": "ser_1783200000000_abc12",
"status": "rendering",
"total": 3,
"completed": 1,
"series_title": "Building a Personal Brand as a Developer",
"posts": [
{ "post_number": 1, "topic": "Why your GitHub is your best portfolio", "style": "revelation::panel", "status": "done", "url": "https://pub-xxx.r2.dev/renders/job1.mp4", "schedule_offset_days": 0, "suggested_time": "08:00" },
{ "post_number": 2, "topic": "The 3-step framework for open-source contributions", "style": "framework::index", "status": "rendering", "url": null, "schedule_offset_days": 2, "suggested_time": "09:00" },
{ "post_number": 3, "topic": "How I landed my first dev job by building in public", "style": "journey::chapter", "status": "queued", "url": null, "schedule_offset_days": 4, "suggested_time": "07:00" }
]
}
Response — scheduled
When status reaches scheduled, all posts have been rendered and inserted into your publishing queue with the correct timestamps. If you provided a callback_url, it has already been called.
API Keys
{ "name": "My App Key" }
Template Styles
Pass a style key in the style field. Leave empty or pass auto — the AI analyses your topic and picks the most fitting template automatically.
revelation::cinematic, a quote gets quote::bold, a comparison gets contrast::versus etc.Styles
Revelation
Stat
Quote
Contrast
Question
Myth
Checklist
Trend
Prediction
Multi-slide
Media — Image requires asset_url
Pass a public image URL in asset_url. AI generates the caption from your topic.
Media — Video overlay requires asset_url (.mp4)
Pass a public .mp4 URL in asset_url. The overlay renders on top of the playing video.
Webhooks
Pass a callback_url in the generate request to receive a notification when the job completes — no polling needed.
Single job payload
{
"job_id": "job_123",
"status": "done",
"output_url": "https://pub-xxx.r2.dev/renders/job_123.png",
"error": null
}
HMAC signature verification
Every webhook includes signature headers so you can verify it came from Frekto:
X-Frekto-Signature: t=1783200000,v1=abc123def456...
X-Frekto-Timestamp: 1783200000
The signature is HMAC-SHA256(timestamp + "." + body, signing_secret). Verify it before trusting the payload:
// Node.js verification
const crypto = require('crypto');
function verifyFrektoWebhook(rawBody, signatureHeader, apiKeyHash) {
const [tPart, v1Part] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const v1 = v1Part.split('=')[1];
// Reject webhooks older than 5 minutes
if (Math.floor(Date.now() / 1000) - parseInt(timestamp) > 300) return false;
const expected = crypto
.createHmac('sha256', apiKeyHash)
.update(timestamp + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
}
// Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-frekto-signature'];
if (!verifyFrektoWebhook(req.body.toString(), sig, YOUR_API_KEY_HASH))
return res.status(401).send('Invalid signature');
const payload = JSON.parse(req.body);
res.sendStatus(200);
});
| Header | Value |
|---|---|
| X-Frekto-Signature | t=<unix_seconds>,v1=<hmac_sha256_hex> |
| X-Frekto-Timestamp | Unix timestamp (seconds) — reject if > 5 min old |
Code Examples
async function generatePost(topic, apiKey, options = {}) {
// Submit job
const res = await fetch('https://api.frekto.ai/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
topic,
format: options.format ?? '4:5',
output_format: options.output_format ?? 'png',
style: options.style ?? null,
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
// Poll until done (every 3s, timeout after 3 min)
const deadline = Date.now() + 180_000;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 3000));
const job = await fetch(`https://api.frekto.ai/jobs/${data.job_id}`, {
headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(r => r.json());
if (job.status === 'done') return job.output_url;
if (job.status === 'failed') throw new Error(job.error ?? 'Render failed');
}
throw new Error('Timed out waiting for render');
}
// Usage
try {
const url = await generatePost(
'AI is transforming healthcare',
'frekto_live_YOUR_KEY',
{ format: '4:5', output_format: 'mp4' }
);
console.log('Generated:', url);
} catch (err) {
console.error('Error:', err.message);
}
import requests, time
def generate_post(topic, api_key, format='4:5', output_format='png', style=None):
headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
# Submit job
res = requests.post(
'https://api.frekto.ai/generate',
headers=headers,
json={'topic': topic, 'format': format, 'output_format': output_format, 'style': style}
)
data = res.json()
if not res.ok:
raise Exception(data.get('error', f'HTTP {res.status_code}'))
job_id = data['job_id']
# Poll until done (timeout after 3 min)
deadline = time.time() + 180
while time.time() < deadline:
time.sleep(3)
job = requests.get(f'https://api.frekto.ai/jobs/{job_id}', headers=headers).json()
if job['status'] == 'done': return job['output_url']
if job['status'] == 'failed': raise Exception(job.get('error', 'Render failed'))
print('Status:', job['status'])
raise Exception('Timed out waiting for render')
# Usage
try:
url = generate_post(
'AI is transforming healthcare',
'frekto_live_YOUR_KEY',
format='4:5', output_format='mp4'
)
print('Generated:', url)
except Exception as e:
print('Error:', e)
# Submit job
curl -X POST https://api.frekto.ai/generate \
-H "Authorization: Bearer frekto_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"topic": "AI is transforming healthcare",
"format": "4:5",
"output_format": "png"
}'
# Poll status
curl https://api.frekto.ai/jobs/job_123 \
-H "Authorization: Bearer frekto_live_YOUR_KEY"
Series Example
Generate a full content series with one call — AI plans the topics, Railway renders each post, and the worker auto-schedules them.
async function generateSeries(instruction, apiKey, options = {}) {
// Start the series
const res = await fetch('https://api.frekto.ai/generate/series', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
instruction,
count: options.count ?? 3,
platform: options.platform ?? 'linkedin',
cadence: options.cadence ?? 'weekdays',
output_format: options.output_format ?? 'mp4',
schedule: options.startDate ? { start_date: options.startDate } : undefined,
callback_url: options.callbackUrl ?? undefined,
})
});
const { series_id } = await res.json();
console.log('Series started:', series_id);
// Poll until scheduled
while (true) {
await new Promise(r => setTimeout(r, 5000));
const status = await fetch(`https://api.frekto.ai/series/${series_id}`, {
headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(r => r.json());
console.log(`${status.completed}/${status.total} complete — ${status.status}`);
status.posts.forEach(p => {
if (p.url) console.log(` Post ${p.post_number}: ${p.url}`);
});
if (status.status === 'scheduled') return status.posts;
if (status.status === 'failed') throw new Error('Series failed');
}
}
// Usage
const posts = await generateSeries(
'5 posts on building a personal brand as a developer',
'frekto_live_YOUR_KEY',
{ count: 3, platform: 'linkedin', startDate: '2026-07-08' }
);
console.log('All scheduled:', posts.map(p => p.url));
import requests, time
def generate_series(instruction, api_key, count=3, platform='linkedin', start_date=None):
headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
# Start the series
body = {'instruction': instruction, 'count': count, 'platform': platform, 'output_format': 'mp4'}
if start_date:
body['schedule'] = {'start_date': start_date}
res = requests.post('https://api.frekto.ai/generate/series', headers=headers, json=body)
series_id = res.json()['series_id']
print(f'Series started: {series_id}')
# Poll until scheduled
while True:
time.sleep(5)
status = requests.get(f'https://api.frekto.ai/series/{series_id}', headers=headers).json()
print(f"{status['completed']}/{status['total']} complete — {status['status']}")
for p in status['posts']:
if p.get('url'):
print(f" Post {p['post_number']}: {p['url']}")
if status['status'] == 'scheduled': return status['posts']
if status['status'] == 'failed': raise Exception('Series failed')
# Usage
posts = generate_series(
'5 posts on building a personal brand as a developer',
'frekto_live_YOUR_KEY',
count=3, start_date='2026-07-08'
)
print('All scheduled:', [p['url'] for p in posts])
# Start series
curl -X POST https://api.frekto.ai/generate/series \
-H "Authorization: Bearer frekto_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"instruction": "5 posts on building a personal brand as a developer",
"count": 3,
"platform": "linkedin",
"output_format": "mp4",
"schedule": { "start_date": "2026-07-08" }
}'
# Poll status (replace SERIES_ID)
curl https://api.frekto.ai/series/SERIES_ID \
-H "Authorization: Bearer frekto_live_YOUR_KEY"
MCP Integration
Use Frekto directly from Claude Desktop or any MCP-compatible AI assistant. Once connected, Claude can generate posts and series by just being asked.
{
"mcpServers": {
"frekto": {
"url": "https://mcp.frekto.ai/mcp",
"headers": {
"Authorization": "Bearer frekto_live_YOUR_KEY"
}
}
}
}
Available tools
| Tool | What it does |
|---|---|
generate_post |
Generate a single post from a topic. Waits for completion and returns the URL. Accepts topic, style, format, output_format, asset_url, callback_url. |
get_job_status |
Check the status of a single render job by job_id. Returns status and output_url when done. |
generate_series |
Generate a full content series from one instruction. AI plans N posts, renders in parallel, auto-schedules. Returns series_id immediately — poll with get_series_status. |
get_series_status |
Poll a series by series_id. Shows per-post progress. When status is "scheduled" all posts are rendered and in the publish queue. |
list_styles |
List all available template styles, optionally filtered by category (revelation, stat, quote, etc.). |
create_api_key |
Create a new Frekto API key for programmatic access. |
list_api_keys |
List all API keys for the current account. |
Example prompts
"Generate a post about AI trends in healthcare as an MP4"
"Create a series of 3 LinkedIn posts about building a personal brand as a developer, starting next Monday"
"What template styles are available for stat posts?"