Frekto API BETA
Generate AI-powered social media posts — images and animated videos — via a simple REST API.
Frekto's API is currently in beta — endpoints and response shapes may change without notice. We recommend pinning to specific behavior in production integrations and watching this page for updates.
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_KEYRate 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. Leave unset if setting post_type: "story" — it's set to 9:16 automatically. |
| post_type | string | optional | Instagram/Facebook only — feed (default), story, or reel. Setting story captures that intent at generation time: format is automatically set to 9:16 unless you explicitly set it yourself, in which case it must also be 9:16 or the request is rejected. Instagram crops non-9:16 assets posted as Stories rather than fitting them, so there's no conversion step after the fact — the correct shape is generated from the start. |
| output_format | string | optional | png, mp4, or carousel. Leave unset to auto-select based on the style's own preferred_output — most styles are image, motion-native styles (montage, media) are mp4, multi-slide styles (framework, journey) are carousel. |
| asset_url | string | optional | Public URL of background image/video. Must allow CORS. |
| callback_url | string | optional | Webhook URL to notify on completion |
| schedule | object or false | optional | Every post is auto-scheduled onto your feed by default (tomorrow at 09:00 UTC if not specified) — pass "schedule": false for render-only. Or an object: { "start_date": "2026-08-25", "time": "09:00", "timezone": "Asia/Kolkata" }, all fields optional. |
"schedule": false explicitly.Your account's default brand (logo, colors, fonts) is applied automatically — no need to specify anything. If you manage multiple brands, you can optionally pass a brand_id to use a different one.
Example request
{
"topic": "AI is transforming healthcare",
"style": "revelation::cinematic",
"format": "4:5"
}Example request — Instagram/Facebook Story
{
"topic": "Behind the scenes at our office",
"post_type": "story",
"schedule": { "post_now": true, "platform": "instagram" }
}Response
{
"job_id": "job_1782400942804_abc123",
"status": "queued",
"format": "4:5",
"post_type": null
}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 | post_type must be one of: feed, story, reel | Invalid post_type value |
| 400 | post_type:"story" requires format:"9:16"... | Explicit format conflicts with post_type:"story" — Instagram would crop the result, so this is rejected instead of silently converting |
| 400 | output_format must be one of: mp4, png, carousel | Invalid output format |
| 400 | output_format 'carousel' requires a multi-slide style | carousel requested for a style that isn't framework::*/journey::* |
| 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 |
The render itself always runs regardless of connected accounts. If no social account is connected when the render completes, scheduling that post is skipped gracefully — the render job still finishes with a real output_url you can use directly. Connect accounts at Settings → Social Accounts before generating if you want automatic scheduling too.
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",
"thumbnail_url": "https://pub-xxx.r2.dev/renders/job_abc123_thumb.jpg",
"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–30, default 3) |
| platform | string | optional | linkedin (default), instagram, facebook, pinterest, youtube |
| cadence | string | optional | Post spacing: weekdays (default), daily, mwf, linkedin |
| format | string | optional | 4:5 (default), 9:16, 1:1. Leave unset if setting post_type: "story" — it's set to 9:16 automatically for the whole series. |
| post_type | string | optional | Instagram/Facebook only — feed (default), story, or reel, applied to every post in the series. Setting story generates the entire series at 9:16 from the start, since Instagram crops non-9:16 assets posted as Stories rather than fitting them. A daily Story cadence is a good fit for keeping a brand visibly active without the higher commitment of daily Feed posts. |
| output_format | string | optional | png, mp4, or carousel. Leave unset — each post's output is auto-selected per its own style, so a series naturally mixes formats instead of forcing every post to one type. |
| 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 |
Your account's default brand (logo, colors, fonts) is applied automatically — no need to specify anything. If you manage multiple brands, you can optionally pass a brand_id to use a different one.
Example request
{
"instruction": "5 posts on building a personal brand as a developer",
"count": 3,
"platform": "linkedin",
"cadence": "weekdays",
"schedule": { "start_date": "2026-07-08" },
"callback_url": "https://yoursite.com/webhook/series"
}Example request — a week of daily Instagram Stories
{
"instruction": "covering varied brand topics",
"count": 7,
"platform": "instagram",
"cadence": "daily",
"post_type": "story"
}Response
{
"series_id": "ser_1783200000000_abc12",
"status": "rendering",
"total": 3,
"series_title": "Building a Personal Brand as a Developer",
"format": "4:5",
"post_type": null
}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.
POST /generate/blog
Write a full LinkedIn article (400–600 words, real web research included) and auto-schedule it. Returns the complete article text immediately — no polling needed, since this is pure text generation with no image/video rendering involved, unlike POST /generate or POST /generate/series.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| topic | string | required | What the article should be about. Max 300 characters. |
| platform | string | optional | linkedin (default) posts as a normal LinkedIn post, using a shorter, SEO-stripped version of the article as the caption. blog_mcp publishes the full article to your own connected blog CMS instead — connect one in the Frekto app under Settings → Integrations first. These are two different destinations for the same generated article, not a translation step. |
| schedule | object or false | optional | Same convention as /generate — every article is auto-scheduled for tomorrow by default. Pass "schedule": false to generate without posting anywhere (e.g. to review the article first), or { "post_now": true } / { "start_date": "2026-08-25" } to control timing. |
Your account's default brand (logo, colors, voice, content pillars) is applied automatically — no need to specify anything.
Example request
{
"topic": "why most SaaS onboarding fails in the first week",
"platform": "linkedin",
"schedule": { "start_date": "2026-08-25" }
}Example request — publish to a connected blog CMS instead
{
"topic": "why most SaaS onboarding fails in the first week",
"platform": "blog_mcp"
}Response
{
"job_id": "job_1789210000000_xyz12",
"status": "done",
"wants_schedule": true,
"platform": "linkedin",
"heading": "Why Most SaaS Onboarding Fails in the First Week",
"body": "Full article text with \\n\\n between paragraphs...",
"hashtags": ["#SaaS", "#ProductLed", "#Onboarding", "#B2B", "#Growth", "#UX"],
"meta_description": "A concise summary under 155 characters...",
"primary_keyword": "SaaS onboarding"
}Unlike /generate, this returns immediately with status: "done" — there's no GET /jobs/:job_id polling step, since blog generation has no rendering to wait for.
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.
Styles
Loading current template styles…
Assets required
Styles marked requires asset_url above need a public image or video URL passed in asset_url — AI generates the caption from your topic. media::video_* styles need a public .mp4 URL specifically; 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: 1783200000The 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',
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.is_carousel ? job.image_urls : 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', style: 'revelation::cinematic' }
);
console.log('Generated:', url);
} catch (err) {
console.error('Error:', err.message);
}import requests, time
def generate_post(topic, api_key, format='4:5', style=None):
headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
# Submit job — output_format omitted, auto-selected per style
res = requests.post(
'https://api.frekto.ai/generate',
headers=headers,
json={'topic': topic, 'format': 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['image_urls'] if job.get('is_carousel') else 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', style='revelation::cinematic'
)
print('Generated:', url)
except Exception as e:
print('Error:', e)# Submit job — output_format omitted, auto-selected per style
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"
}'
# 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',
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 — output_format omitted, auto-selected per post's style
body = {'instruction': instruction, 'count': count, 'platform': platform}
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",
"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 — Claude.ai, Claude Desktop, Cowork, or any other MCP-compatible assistant. Once connected, Claude can generate posts and series by just being asked.
Sign in with your Frekto account directly — no API key to copy, no config file. Works on Pro, Max, Team, and Enterprise plans, on every account regardless of any feature rollout.
- In Claude.ai, go to Settings → Connectors
- Click Add connector, then choose Remote
- Name it
frekto, and paste the server URL:https://mcp.frekto.ai/mcp - Click Add — Claude will open a real Frekto sign-in / authorize screen. Log in (or continue if already signed in) and click Allow
- Frekto's tools are now available in any new conversation via the + icon
On Team or Enterprise plans, an Owner adds the connector once via Organization Settings → Connectors; members then connect individually and authorize with their own Frekto account.
Available if your account has Claude.ai's request-header field in the Connector dialog — this feature is on a gradual rollout, so it may not appear on every account yet.
- In Claude.ai, go to Settings → Connectors
- Click Add connector, then choose Remote
- Name it
frekto, and paste the server URL:https://mcp.frekto.ai/mcp - Open Advanced settings and add a header:
Authorization→Bearer frekto_live_YOUR_KEY - Click Add — Frekto's tools are now available in any new conversation via the + icon
Edit claude_desktop_config.json directly and restart Claude Desktop.
{
"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. Returns a job_id immediately (does not wait) — poll with get_job_status. Accepts topic, style, format, output_format (leave unset to auto-select based on style), asset_url, callback_url, and optional schedule_date/schedule_time to auto-schedule once rendered. |
get_job_status | Check the status of a single render job by job_id. Returns status, output_url (or image_urls for a carousel), and scheduled_id once done. |
generate_series | Generate a full content series from one instruction. AI plans N posts with varied topics and styles, renders them, 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_recent_posts | List your recently created single posts. Paginated via limit/before. |
list_recent_series | List your recently created series. Paginated via limit/before. |
list_scheduled_posts | List what's on your scheduling calendar (pending, posted, dead/retrying) — distinct from the two tools above, which list what's been rendered rather than scheduled. |
schedule_post | Schedule an already-rendered post that wasn't scheduled at creation time — post_now for immediately (within about a minute) or a future start_date. |
reschedule_post | Change the date/time of a post that's already scheduled. Takes scheduled_id, not job_id. |
cancel_scheduled_post | Remove a post from the scheduled feed. Takes scheduled_id, not job_id. The original render is unaffected. |
list_styles | List all available template styles, optionally filtered by category (revelation, framework, 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"
"Create a series of 3 LinkedIn posts about building a personal brand as a developer, starting next Monday"
"What have I made recently?"
"Schedule that post for tomorrow at 9am"
"Post that right now"