Public API v1 Reference
Quick links: Authentication β’ Endpoints β’ Rate Limiting β’ Errors β’ Examples
Overviewβ
The Soku Public API enables server-to-server content publishing across multiple social platforms. Built on Firebase Cloud Functions with Express.js, the API provides:
- β Multi-platform publishing to 7 social networks
- β Media processing with automatic normalization
- β AI transcription for audio/video content
- β Template rendering for dynamic OG images
- β Tiered rate limiting based on subscription status
- β Comprehensive error handling with request tracking
Base URL: https://<region>-<project>.cloudfunctions.net/api
API Version: v1
Content-Type: application/json
Related Documentationβ
- Rate Limiting - Request limits, tiers, and best practices
- API Key Lifecycle - Creating and managing API keys
- Publishing Guide - Step-by-step tutorial
- Firestore Data Model - Database schema reference
- Orchestration - Publishing architecture
Authenticationβ
- Generate an API key from the web app Settings β API Keys, or call the web route
POST /api/api-keys(requires session cookie). - Send the key as header
soku-api-key. - Keys are hashed and stored; if revoked, calls return 401.
Subscription requirementβ
All Public API requests require an active subscription. Requests authenticate the API key and also verify that the callerβs subscription is in an active state.
- Allowed states:
trialing,activewithcurrentPeriodEnd > now - Responses when inactive/expired:
403 forbiddenwith codeforbidden - Effected endpoints:
/v1/posts,/v1/media
Endpointsβ
POST /v1/postsβ
Create a post submission for orchestration across platforms. The payload mirrors the internal orchestrator request.
Request body (preferred shape):
{
"post": {
"content": {
"text": "string",
"mediaType": "video | image | text",
"mediaUrls": ["https://..."],
"platform": [
{ "platform": "instagram", "accountId": "ig_creator_main" },
{ "platform": "facebook", "accountId": "fb_page_42" },
"threads"
]
}
},
"scheduledTime": "2025-01-31T20:00:00Z"
}
-
Notes:
-
Targets are detected from
post.content.platform. -
Each entry may be a string (legacy shape) or an object:
platform(required):"facebook" | "instagram" | "threads" | "x" | "tiktok" | "youtube" | "linkedin"accountId(required when the user has multiple accounts on that platform): integration account ID underusers/{uid}/integrations/{platform}/accounts/{accountId}targetId(optional but recommended): client-specified dedupe key (${platform}:${accountId}) for idempotency
-
If
accountIdis omitted and the platform has exactly one connected account, the API infers it. Otherwise the call fails with400 missing_account. -
Legacy string entries (
"threads") remain supported only whileconnectedCount === 1for that provider; once a second account is connected, calls must supply explicit objects. -
Soku deduplicates runs per
targetId. When not provided, we default to${platform}:${accountId}. -
mediaTypeis optional; if omitted, we infer from the URL. WhenmediaType === "text",mediaUrlsmay be omitted. -
If
scheduledTimeis in the future, the job is enqueued with a delay; otherwise immediate orchestration.
Response:
{ "postSubmissionId": "<uuid>" }
Lifecycle (Firestore):
postSubmissions/{postSubmissionId}:{ userId, keyHash, status: 'queued'|'orchestrating'|'dispatched', targets, createdAt, ... }- Per-platform runs:
postSubmissions/{id}/runs/{platform}with{ status: 'queued'|'processing'|'succeeded'|'failed', attemptCount, timestamps, result }
/v1/repurposeLinksβ
Manage repost/repurpose automations for a specific platform account. These routes live on the Public API surface so SDKs, partners, and Zapier actions can stay in sync with what the web app writes to users/{uid}/repurposeLinks/{linkId}.
GET /v1/repurposeLinksβ
- Optional query params:
platform: filter to a single platform (e.g.,instagram)accountId: filter to a specific integration accounttype:'text' | 'image' | 'video'
- Response:
{
"links": [
{
"id": "link_abc",
"platform": "instagram",
"accountId": "ig_creator_main",
"type": "image",
"templateId": "tmpl_123",
"settings": { "delayHours": 12 },
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-01T00:00:00Z"
}
]
}
POST /v1/repurposeLinksβ
Request body:
{
"platform": "tiktok",
"accountId": "tt_brand",
"type": "video",
"templateId": "tmpl_storyboard",
"settings": { "delayHours": 6 }
}
platformrequiredaccountIdrequired whenever the user has multiple accounts on the platform. If omitted and we detect a single connected account we will set it automatically; otherwise we respond with400 missing_account.typerequired:'text' | 'image' | 'video'templateIdrequired: maps totemplates/{id}settings.delayHoursoptional integer (>= 0)
Response: { "id": "link_abc" }
Server behavior:
- Writes to
users/{uid}/repurposeLinks/{id} - Validates that the referenced
accountIdexists and that the target account has repost/repurpose toggles enabled
DELETE /v1/repurposeLinks/{id}β
- Deletes the link doc and returns
{ "deleted": true } - Idempotent: deleting a missing id also returns
{ "deleted": true } - If the link targeted a template that is no longer referenced anywhere else, the orchestrator simply skips it.
POST /v1/mediaβ
Fetch and persist remote media to Cloud Storage and return a signed download URL.
Request body:
{ "url": "https://example.com/file.jpg" }
Response:
{ "url": "https://storage.mysoku.io/<id>.<ext>" }
Notes:
- Stored under
media/<id>.<ext>in bucketMEDIA_BUCKET. - A Firebase Storage download token is attached; objects are not made public.
- A
mediacollection doc{ id, userId, keyHash, url, contentType, objectPath, downloadToken, originalUrl, finalUrl, ext, createdAt }is created. - The returned URL uses a public base (
MEDIA_PUBLIC_BASE) that proxies/streams the object via themediafunction.
POST /v1/ai/transcribeβ
Transcribe audio or video media using AI (OpenAI Whisper). Requires an active subscription and debits AI credits from the user's account.
Request body:
{
"mediaId": "string",
"language": "en",
"model": "whisper-1",
"durationSeconds": 120
}
mediaId(required): ID of media document from/v1/mediaendpointlanguage(optional): ISO 639-1 language code (e.g., "en", "es", "fr"). Defaults to auto-detectionmodel(optional): OpenAI model to use. Defaults to "whisper-1"durationSeconds(optional): Media duration in seconds for credit calculation
Response:
{
"jobId": "string",
"transcript": "Transcribed text here...",
"creditsDebited": 10,
"likelyLyrics": false
}
jobId: Unique identifier for this transcription jobtranscript: Full transcribed textcreditsDebited: Number of AI credits charged for this operationlikelyLyrics: Boolean indicating if the content appears to be song lyrics
Optional header:
Idempotency-Key: Prevent duplicate charges for the same transcription
Notes:
- Job records are stored in
users/{uid}/aiJobs/{jobId} - Transcripts are also saved to
media/{mediaId}/transcripts/{jobId} - Credits are debited from
users/{uid}/credits/currentand logged tocreditLedger - Only audio and video MIME types are supported (checked via
contentType)
Errors:
MEDIA_NOT_FOUND: mediaId does not existMEDIA_INVALID: Media document missing required fieldsUNSUPPORTED_MEDIA_TYPE: Media is not audio or video
POST /v1/templatesβ
Render an OG (Open Graph) image template and return a hosted URL. This endpoint renders a dynamic template via the web application and saves the result to Cloud Storage.
Request body:
{
"templateSlug": "tweetImage",
"config": {
"title": "My Tweet",
"author": "@username",
"content": "Tweet text here"
}
}
templateSlug(required): Template identifier (e.g., "tweetImage")config(optional): Object containing template-specific configuration parameters
Response:
{
"url": "https://storage.mysoku.io/<id>.png",
"rawUrl": "https://firebasestorage.googleapis.com/...",
"id": "uuid"
}
url: Friendly public URL for the rendered image (proxied via media function)rawUrl: Direct Firebase Storage URL with download tokenid: Unique identifier for the rendered media
Notes:
- Templates are rendered by fetching
/og/{templateSlug}?config=<base64>from the web app - Rendered images are stored in
rendered_media/{id}collection with metadata - The endpoint also supports an alias route:
POST /v1/media/render-og(same functionality)
API key management (user-scoped)β
These routes require a Firebase ID token (not an API key). They are used by the web app to manage keys.
POST /v1/api-keysβ returns{ apiKey }GET /v1/api-keysβ returns{ keys: [{ id, name, createdAt, revokedAt, tokenPreview }] }DELETE /v1/api-keys/{id}β returns{ revoked: true }
Errorsβ
The API uses a comprehensive error handling system with structured responses and request tracking.
Error Response Formatβ
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"timestamp": "2025-08-29T00:02:14.654Z",
"requestId": "abb6930d-2b57-4d44-9d63-19d5a9c8d077",
"details": {}
}
}
Common Error Codesβ
| Code | Status | Description |
|---|---|---|
unauthorized | 401 | Missing or invalid API key |
forbidden | 403 | Subscription inactive or expired |
not_found | 404 | Resource not found |
validation_error | 400 | Request validation failed (Zod schema errors) |
rate_limit_exceeded | 429 | Too many requests (check Retry-After header) |
missing_integrations | 400 | Required platform integration not connected |
missing_account | 400 | Multiple accounts exist, accountId required |
dispatch_failed | 500 | Failed to enqueue publishing tasks |
post_creation_failed | 500 | Failed to create post submission |
insufficient_credits | 402 | Not enough AI credits for operation |
internal_error | 500 | Server error |
Multi-Account Error Handlingβ
When working with platforms that have multiple connected accounts:
Error: missing_integrations
{
"error": {
"code": "missing_integrations",
"message": "Missing integrations for platforms: instagram:ig_creator_main",
"timestamp": "2025-08-29T00:02:14.654Z",
"requestId": "..."
}
}
Cause: The specified accountId doesn't exist or the platform isn't connected.
Solution: Check connected integrations via web app or verify accountId spelling.
Error: missing_account (when implementation requires explicit accountId)
{
"error": {
"code": "missing_account",
"message": "Multiple Instagram accounts connected. Please specify accountId.",
"timestamp": "2025-08-29T00:02:14.654Z",
"requestId": "..."
}
}
Cause: User has multiple accounts on a platform but request didn't specify accountId.
Solution: Update request to include explicit accountId in platform array:
{
"post": {
"content": {
"platform": [
{ "platform": "instagram", "accountId": "ig_creator_main" }
]
}
}
}
Request Trackingβ
Include X-Request-ID header for request tracking:
curl -H "X-Request-ID: debug-123" \
-H "soku-api-key: your_key" \
https://api.example.com/v1/posts
Error Loggingβ
Errors are logged to:
- Firestore:
errorLogscollection - Console: Immediate visibility during development
- Headers: Request ID for correlation
Development Modeβ
Set NODE_ENV=development for detailed error responses including stack traces.
Credit System Errorsβ
Error: insufficient_credits
{
"error": {
"code": "insufficient_credits",
"message": "Insufficient AI credits. Required: 10, Available: 5",
"timestamp": "2025-08-29T00:02:14.654Z",
"requestId": "...",
"details": {
"required": 10,
"available": 5,
"feature": "transcription"
}
}
}
Cause: Not enough AI credits in current billing period.
Solution:
- Check credit balance:
GET /v1/credits/balance(if implemented) - Wait for next billing period
- Upgrade subscription plan
- Contact support for credit top-up
Rate Limit Headersβ
All responses include rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 2025-08-29T00:03:00.000Z
X-RateLimit-Tier: premium
When rate limited (429), response includes:
Retry-After: 42
Rate Limit Tiers:
- Default: 25 req/min (unauthenticated)
- Authenticated: 60 req/min (valid API key)
- Premium: 100 req/min (active subscription)
- Admin: 200 req/min (whitelisted users)
See Rate Limiting for detailed documentation.
Examplesβ
Basic Post Submissionβ
Create a text post to multiple platforms:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"post": {
"content": {
"text": "Hello world",
"platform": [
{ "platform": "x", "accountId": "x_main" },
"threads"
]
}
}
}'
Response:
{
"postSubmissionId": "550e8400-e29b-41d4-a716-446655440000"
}
Multi-Account Postingβ
Post to multiple accounts on the same platform:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"post": {
"content": {
"text": "Exciting announcement!",
"mediaUrls": ["https://example.com/image.jpg"],
"platform": [
{ "platform": "instagram", "accountId": "ig_personal" },
{ "platform": "instagram", "accountId": "ig_business" },
{ "platform": "facebook", "accountId": "fb_page_main" }
]
}
}
}'
Video Post with Carouselβ
Post video to TikTok and carousel to Instagram:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"post": {
"content": {
"text": "Check out my latest video!",
"videoUrl": "https://example.com/video.mp4",
"imageUrls": [
"https://example.com/slide1.jpg",
"https://example.com/slide2.jpg",
"https://example.com/slide3.jpg"
],
"platform": [
{ "platform": "tiktok", "accountId": "tt_creator" },
{ "platform": "instagram", "accountId": "ig_main" }
]
}
}
}'
Scheduled Postβ
Schedule a post for future publication:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"post": {
"content": {
"text": "Good morning! βοΈ",
"platform": ["threads", "x"]
}
},
"scheduledTime": "2025-09-01T08:00:00Z"
}'
Upload Remote Mediaβ
Fetch and store remote media in Cloud Storage:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/media" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{ "url": "https://picsum.photos/seed/soku/1200/675.jpg" }'
Response:
{
"url": "https://storage.mysoku.io/abc123.jpg"
}
Use Case: Pre-upload media to get stable URLs that work across all platforms.
Using Uploaded Media in Postsβ
Combine media upload with post submission:
# Step 1: Upload media
MEDIA_URL=$(curl -s -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/media" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{ "url": "https://example.com/video.mp4" }' | jq -r '.url')
# Step 2: Create post with uploaded media
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d "{
\"post\": {
\"content\": {
\"text\": \"Check this out!\",
\"videoUrl\": \"$MEDIA_URL\",
\"platform\": [\"tiktok\", \"youtube\"]
}
}
}"
AI Transcriptionβ
Transcribe audio/video with OpenAI Whisper:
# Step 1: Upload media to get mediaId
RESPONSE=$(curl -s -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/media" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{ "url": "https://example.com/podcast.mp3" }')
MEDIA_ID=$(echo $RESPONSE | jq -r '.id')
# Step 2: Transcribe
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/ai/transcribe" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-H "Idempotency-Key: unique-job-id-123" \
-d "{
\"mediaId\": \"$MEDIA_ID\",
\"language\": \"en\",
\"durationSeconds\": 120
}"
Response:
{
"jobId": "job_abc123",
"transcript": "Welcome to my podcast. Today we're discussing...",
"creditsDebited": 10,
"likelyLyrics": false
}
Credit Calculation: ~1 credit per minute of audio/video (varies by model).
Idempotency: Use Idempotency-Key header to prevent duplicate charges if request is retried.
Template Renderingβ
Render dynamic OG images from templates:
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/templates" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"templateSlug": "tweetImage",
"config": {
"title": "My Custom Tweet",
"author": "@username",
"content": "This is my tweet content"
}
}'
Response:
{
"url": "https://storage.mysoku.io/rendered_abc123.png",
"rawUrl": "https://firebasestorage.googleapis.com/v0/b/...",
"id": "rendered_abc123"
}
Use Case: Generate custom social media graphics programmatically.
Using Rendered Templates in Postsβ
# Step 1: Render template
TEMPLATE_RESPONSE=$(curl -s -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/templates" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d '{
"templateSlug": "tweetImage",
"config": {
"title": "Big Announcement!",
"author": "@mybrand",
"content": "We just launched our new product π"
}
}')
TEMPLATE_URL=$(echo $TEMPLATE_RESPONSE | jq -r '.url')
# Step 2: Post with rendered image
curl -X POST "https://<region>-<project>.cloudfunctions.net/api/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $SOKU_API_KEY" \
-d "{
\"post\": {
\"content\": {
\"text\": \"Big news! π\",
\"imageUrls\": [\"$TEMPLATE_URL\"],
\"platform\": [\"x\", \"threads\", \"linkedin\"]
}
}
}"
Complete Workflow Exampleβ
Here's a complete workflow combining multiple API endpoints:
#!/bin/bash
set -e
API_KEY="your_api_key_here"
BASE_URL="https://us-central1-your-project.cloudfunctions.net/api"
echo "1. Uploading video..."
VIDEO_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/media" \
-H "Content-Type: application/json" \
-H "soku-api-key: $API_KEY" \
-d '{
"url": "https://example.com/my-video.mp4"
}')
VIDEO_URL=$(echo $VIDEO_RESPONSE | jq -r '.url')
MEDIA_ID=$(echo $VIDEO_RESPONSE | jq -r '.id')
echo "Video uploaded: $VIDEO_URL"
echo "2. Transcribing video..."
TRANSCRIPT_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/ai/transcribe" \
-H "Content-Type: application/json" \
-H "soku-api-key: $API_KEY" \
-H "Idempotency-Key: workflow-$(date +%s)" \
-d "{
\"mediaId\": \"$MEDIA_ID\",
\"language\": \"en\",
\"durationSeconds\": 180
}")
TRANSCRIPT=$(echo $TRANSCRIPT_RESPONSE | jq -r '.transcript')
CREDITS_USED=$(echo $TRANSCRIPT_RESPONSE | jq -r '.creditsDebited')
echo "Transcript: ${TRANSCRIPT:0:100}..."
echo "Credits used: $CREDITS_USED"
echo "3. Rendering custom thumbnail..."
TEMPLATE_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/templates" \
-H "Content-Type: application/json" \
-H "soku-api-key: $API_KEY" \
-d "{
\"templateSlug\": \"videoThumbnail\",
\"config\": {
\"title\": \"New Video!\",
\"subtitle\": \"${TRANSCRIPT:0:50}...\"
}
}")
THUMBNAIL_URL=$(echo $TEMPLATE_RESPONSE | jq -r '.url')
echo "Thumbnail rendered: $THUMBNAIL_URL"
echo "4. Publishing to multiple platforms..."
POST_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/posts" \
-H "Content-Type: application/json" \
-H "soku-api-key: $API_KEY" \
-d "{
\"post\": {
\"content\": {
\"text\": \"Check out my latest video! ${TRANSCRIPT:0:100}...\",
\"videoUrl\": \"$VIDEO_URL\",
\"imageUrls\": [\"$THUMBNAIL_URL\"],
\"platform\": [
{ \"platform\": \"tiktok\", \"accountId\": \"tt_main\" },
{ \"platform\": \"youtube\", \"accountId\": \"yt_channel\" },
{ \"platform\": \"instagram\", \"accountId\": \"ig_creator\" }
]
}
}
}")
SUBMISSION_ID=$(echo $POST_RESPONSE | jq -r '.postSubmissionId')
echo "Post submitted: $SUBMISSION_ID"
echo "Done! β
"
Multi-account Test Plan (SDK / Zapier)β
| Audience | Scenario | Expected Result |
|---|---|---|
| SDK clients | POST /v1/posts with platform: [{ "platform": "instagram", "accountId": "acct-a" }, { "platform": "instagram", "accountId": "acct-b" }] | Two unique runs are created (instagram:acct-a, instagram:acct-b) and appear independently in postSubmissions/{id}/runs/*. |
| SDK clients | POST /v1/posts with missing accountId while multiple accounts exist | API responds with 400 missing_integrations; callers should prompt the user to reconnect or specify the account. |
| Zapier | Zap action "Publish Post" configured with account pick list that passes accountId | The Cloud Function payload includes the selected accountId; only that account publishes. |
| Zapier | Zap sends duplicate targets with explicit targetId | Server dedupes on targetId; Zap logs should show a single run for that identifier. |
| Regression | Legacy payload ["x","threads"] when a platform has a single account | Validation accepts the legacy shape, resolves default accounts, and executes successfully. |
| SDK clients | POST /v1/repurposeLinks with { platform, accountId, type, templateId, settings } | Firestore stores the link as users/{uid}/repurposeLinks/{id} scoped to that accountId; GET filtered by the same account returns exactly one record. |
| SDK clients | DELETE /v1/repurposeLinks/{id} for account acct-a while another account has links | Only the targeted link is removed; other accounts' repurpose links remain untouched. |
| Zapier | GET /v1/repurposeLinks?platform=instagram&accountId=acct-b powering a dropdown | Response list contains only templates for acct-b, ensuring Zapier users cannot accidentally edit acct-a automations. |
Record the postSubmissionId for each scenario above and attach screenshots/logs before promoting a build to staging or production.
Related Documentationβ
API Documentationβ
- Rate Limiting - Understand request limits and tiers
- API Key Lifecycle - Manage your API keys
- Functions API - Internal OAuth & publish endpoints
- Web API - Next.js API routes
- Media API - Media proxy endpoint
Guides & Tutorialsβ
- Publishing via Public API - Complete publishing tutorial
- Media Normalization - How media processing works
- Local Development - Test the API locally
Architectureβ
- Orchestration - Publishing system architecture
- End-to-End Flow - Complete request lifecycle
- Cloud Tasks - Queue configuration
Data Modelβ
- Firestore Collections - Complete database schema
postSubmissions/{id}- Submission trackingpostSubmissions/{id}/runs/{platform}- Per-platform executionusers/{uid}/apiRequests- Request loggingusers/{uid}/aiJobs- Transcription jobsmedia/{id}- Uploaded media files
Operationsβ
- Monitoring - Track API usage and errors
- Troubleshooting - Common issues and solutions
- Security - Security best practices