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.

Base URL https://api.frekto.ai

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
Get an API key — contact the Frekto team or create one from your dashboard at app.frekto.ai.

Rate Limits

10
renders / day — Free
30
renders / day — Paid

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.

POST /generate

Request body

FieldTypeRequiredDescription
topicstringrequiredTopic or subject for the post. Max 300 characters.
stylestringoptionalTemplate style key e.g. revelation::cinematic. Omit or pass auto for AI to pick the best fit based on your topic. See styles.
formatstringoptional4:5 (default), 9:16, 1:1. Leave unset if setting post_type: "story" — it's set to 9:16 automatically.
post_typestringoptionalInstagram/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_formatstringoptionalpng, 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_urlstringoptionalPublic URL of background image/video. Must allow CORS.
callback_urlstringoptionalWebhook URL to notify on completion
scheduleobject or falseoptionalEvery 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.
Every post schedules itself for tomorrow by default. This is intentional — a rendered post with nowhere to land is invisible everywhere in the app. If you want to generate without scheduling, pass "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

StatusErrorCause
401Invalid or missing API keyMissing or wrong Authorization header
400topic is requiredMissing or empty topic
400topic must be 300 characters or lessTopic too long
400format must be one of: 4:5, 9:16, 1:1Invalid format value
400post_type must be one of: feed, story, reelInvalid post_type value
400post_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
400output_format must be one of: mp4, png, carouselInvalid output format
400output_format 'carousel' requires a multi-slide stylecarousel requested for a style that isn't framework::*/journey::*
400Unknown style "..."Style key not in the valid list
400style "media::..." requires asset_urlMedia styles need a public image/video URL
400Request body must be valid JSONMalformed request body
⚠️ Social account required for scheduling

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.

GET /jobs/:job_id

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.

POST /generate/series

Request body

FieldTypeRequiredDescription
instructionstringrequiredWhat the series should be about. Max 500 characters.
countnumberoptionalNumber of posts to generate (2–30, default 3)
platformstringoptionallinkedin (default), instagram, facebook, pinterest, youtube
cadencestringoptionalPost spacing: weekdays (default), daily, mwf, linkedin
formatstringoptional4: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_typestringoptionalInstagram/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_formatstringoptionalpng, 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_datestringoptionalISO date string for first post. Defaults to tomorrow.
callback_urlstringoptionalWebhook 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.

POST /generate/blog

Request body

FieldTypeRequiredDescription
topicstringrequiredWhat the article should be about. Max 300 characters.
platformstringoptionallinkedin (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.
scheduleobject or falseoptionalSame 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.

GET /series/:series_id

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

POST /api-keys Create key
{ "name": "My App Key" }
⚠️ Save the key immediately — it is only shown once.
GET /api-keys List keys
DEL /api-keys/:id Revoke 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.

Auto selection — Claude analyses the topic and selects the best template for your content automatically.

Styles

auto
AI picks the best template based on your topic

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: 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);
});
HeaderValue
X-Frekto-Signaturet=<unix_seconds>,v1=<hmac_sha256_hex>
X-Frekto-TimestampUnix timestamp (seconds) — reject if > 5 min old

Code Examples

JavaScript
Python
cURL
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.

JavaScript
Python
cURL
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.

Claude.ai — Custom Connector (recommended)

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.

  1. In Claude.ai, go to Settings → Connectors
  2. Click Add connector, then choose Remote
  3. Name it frekto, and paste the server URL: https://mcp.frekto.ai/mcp
  4. Click Add — Claude will open a real Frekto sign-in / authorize screen. Log in (or continue if already signed in) and click Allow
  5. 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.

Claude.ai — Custom Connector via API key (alternative)

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.

  1. In Claude.ai, go to Settings → Connectors
  2. Click Add connector, then choose Remote
  3. Name it frekto, and paste the server URL: https://mcp.frekto.ai/mcp
  4. Open Advanced settings and add a header: AuthorizationBearer frekto_live_YOUR_KEY
  5. Click Add — Frekto's tools are now available in any new conversation via the + icon
Claude Desktop config (manual)

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

ToolWhat it does
generate_postGenerate 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_statusCheck 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_seriesGenerate 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_statusPoll a series by series_id. Shows per-post progress. When status is "scheduled" all posts are rendered and in the publish queue.
list_recent_postsList your recently created single posts. Paginated via limit/before.
list_recent_seriesList your recently created series. Paginated via limit/before.
list_scheduled_postsList 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_postSchedule 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_postChange the date/time of a post that's already scheduled. Takes scheduled_id, not job_id.
cancel_scheduled_postRemove a post from the scheduled feed. Takes scheduled_id, not job_id. The original render is unaffected.
list_stylesList all available template styles, optionally filtered by category (revelation, framework, quote, etc.).
create_api_keyCreate a new Frekto API key for programmatic access.
list_api_keysList 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"