Skip to main content

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


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, active with currentPeriodEnd > now
  • Responses when inactive/expired: 403 forbidden with code forbidden
  • 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 under users/{uid}/integrations/{platform}/accounts/{accountId}
    • targetId (optional but recommended): client-specified dedupe key (${platform}:${accountId}) for idempotency
  • If accountId is omitted and the platform has exactly one connected account, the API infers it. Otherwise the call fails with 400 missing_account.

  • Legacy string entries ("threads") remain supported only while connectedCount === 1 for 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}.

  • mediaType is optional; if omitted, we infer from the URL. When mediaType === "text", mediaUrls may be omitted.

  • If scheduledTime is 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 }

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}.

  • Optional query params:
    • platform: filter to a single platform (e.g., instagram)
    • accountId: filter to a specific integration account
    • type: '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 }
}
  • platform required
  • accountId required 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 with 400 missing_account.
  • type required: 'text' | 'image' | 'video'
  • templateId required: maps to templates/{id}
  • settings.delayHours optional integer (>= 0)

Response: { "id": "link_abc" }

Server behavior:

  • Writes to users/{uid}/repurposeLinks/{id}
  • Validates that the referenced accountId exists 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 bucket MEDIA_BUCKET.
  • A Firebase Storage download token is attached; objects are not made public.
  • A media collection 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 the media function.

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/media endpoint
  • language (optional): ISO 639-1 language code (e.g., "en", "es", "fr"). Defaults to auto-detection
  • model (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 job
  • transcript: Full transcribed text
  • creditsDebited: Number of AI credits charged for this operation
  • likelyLyrics: 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/current and logged to creditLedger
  • Only audio and video MIME types are supported (checked via contentType)

Errors:

  • MEDIA_NOT_FOUND: mediaId does not exist
  • MEDIA_INVALID: Media document missing required fields
  • UNSUPPORTED_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 token
  • id: 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​

CodeStatusDescription
unauthorized401Missing or invalid API key
forbidden403Subscription inactive or expired
not_found404Resource not found
validation_error400Request validation failed (Zod schema errors)
rate_limit_exceeded429Too many requests (check Retry-After header)
missing_integrations400Required platform integration not connected
missing_account400Multiple accounts exist, accountId required
dispatch_failed500Failed to enqueue publishing tasks
post_creation_failed500Failed to create post submission
insufficient_credits402Not enough AI credits for operation
internal_error500Server 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: errorLogs collection
  • 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:

  1. Check credit balance: GET /v1/credits/balance (if implemented)
  2. Wait for next billing period
  3. Upgrade subscription plan
  4. 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)​

AudienceScenarioExpected Result
SDK clientsPOST /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 clientsPOST /v1/posts with missing accountId while multiple accounts existAPI responds with 400 missing_integrations; callers should prompt the user to reconnect or specify the account.
ZapierZap action "Publish Post" configured with account pick list that passes accountIdThe Cloud Function payload includes the selected accountId; only that account publishes.
ZapierZap sends duplicate targets with explicit targetIdServer dedupes on targetId; Zap logs should show a single run for that identifier.
RegressionLegacy payload ["x","threads"] when a platform has a single accountValidation accepts the legacy shape, resolves default accounts, and executes successfully.
SDK clientsPOST /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 clientsDELETE /v1/repurposeLinks/{id} for account acct-a while another account has linksOnly the targeted link is removed; other accounts' repurpose links remain untouched.
ZapierGET /v1/repurposeLinks?platform=instagram&accountId=acct-b powering a dropdownResponse 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.


API Documentation​

Guides & Tutorials​

Architecture​

Data Model​

  • Firestore Collections - Complete database schema
    • postSubmissions/{id} - Submission tracking
    • postSubmissions/{id}/runs/{platform} - Per-platform execution
    • users/{uid}/apiRequests - Request logging
    • users/{uid}/aiJobs - Transcription jobs
    • media/{id} - Uploaded media files

Operations​