developer API v1

build on dropspace — create content, generate media, and automate multi-platform publishing from your own apps.

getting started

create an API key

go to settings → API keys in your dropspace dashboard and click create key. give it a name, then copy the key immediately — it is shown only once.

store your key securely. keys begin with ds_live_ and cannot be retrieved after creation. if you lose it, revoke and create a new one.

base URL

https://api.dropspace.dev

all timestamps in API responses use ISO 8601 format in UTC (e.g. 2026-02-18T12:00:00.000Z).

make your first request

curl https://api.dropspace.dev/launches \
  -H "Authorization: Bearer ds_live_YOUR_KEY"

quick example

create a launch with generated content, review it, optionally regenerate, edit, and publish.

step 1 — create a launch

curl -X POST https://api.dropspace.dev/launches \
  -H "Authorization: Bearer ds_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "announcing our new feature",
    "product_url": "https://example.com/feature",
    "product_description": "a blazing-fast widget for your dashboard",
    "platforms": ["linkedin", "twitter", "reddit"]
  }'

step 2 — review and optionally regenerate

# regenerate content for specific platforms:
curl -X POST https://api.dropspace.dev/launches/LAUNCH_ID/generate-content \
  -H "Authorization: Bearer ds_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platforms": ["twitter"] }'

# or edit content directly via PATCH:
curl -X PATCH https://api.dropspace.dev/launches/LAUNCH_ID \
  -H "Authorization: Bearer ds_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platform_contents": { "twitter": { "content": "custom tweet..." } } }'

# or use thread array for explicit tweet boundaries:
curl -X PATCH https://api.dropspace.dev/launches/LAUNCH_ID \
  -H "Authorization: Bearer ds_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platform_contents": { "twitter": { "thread": ["tweet 1", "tweet 2", "tweet 3"] } } }'

step 3 — publish

curl -X POST https://api.dropspace.dev/launches/LAUNCH_ID/publish \
  -H "Authorization: Bearer ds_live_YOUR_KEY"

step 4 — check status

curl https://api.dropspace.dev/launches/LAUNCH_ID/status \
  -H "Authorization: Bearer ds_live_YOUR_KEY"

authentication & rate limits

bearer token

every request requires an Authorization header:

Authorization: Bearer ds_live_YOUR_KEY

invalid or revoked keys return 401 with error code AUTH_001.

rate limits

limitvaluescope
general API60 req/minper API key
content generation30 req/minper user
media generation5 req/minper user
video concurrency2 per user / 20 globalconcurrent jobs

when rate-limited you receive a 429 response with a Retry-After header (seconds):

{
  "error": {
    "code": "RATE_001",
    "message": "too many requests. please try again in 12 seconds."
  },
  "retryAfter": 12,
  "limit": 60,
  "remaining": 0
}

rate-limit headers

headerdescription
X-RateLimit-Limitrequests allowed per window
X-RateLimit-Remainingrequests remaining
X-RateLimit-ResetUnix timestamp in milliseconds when window resets (not seconds)

scopes

api keys can be created with specific scopes to limit their permissions. this provides defense-in-depth — even if a key is compromised, it can only perform operations within its scopes.

available scopes

scopedescription
readview launches, personas, media, and connections
writecreate and update launches and personas
deletedelete launches and personas
publishpublish and retry launches
generateAI content and media generation
adminmanage api keys and webhooks

missing scope error

requests without the required scope receive a 403 response:

{
  "error": {
    "code": "AUTH_003",
    "message": "api key missing required scope: publish"
  }
}

recommended scopes by use case

use casescopes
AI agents / MCPread, write, generate
CI/CD publishingread, publish
monitoring / dashboardsread
full access (admin tools)all scopes

audit logging

destructive and sensitive operations performed via the API are automatically logged for security auditing.

audited operations

operationdescription
delete launchpermanently removes a launch
delete personapermanently removes a persona
publish launchpublishes content to connected platforms
create api keygenerates a new api key
revoke api keypermanently disables an api key
create webhookregisters a new webhook endpoint
delete webhookremoves a webhook endpoint
rotate webhook secretgenerates a new signing secret for a webhook

MCP server

the model context protocol(MCP) lets AI agents interact with external tools. the dropspace MCP server gives agents like Claude Code and Cursor the ability to manage launches, personas, media, API keys, webhooks, and more — 37 tools across 9 categories, all from your editor.

install

install the MCP server
npx -y @jclvsh/dropspace

available tools

categorytoolscount
launcheslist, create, get, update, delete, publish, retry, generate content, retry content, get analytics, get status11
postsdelete post, delete all posts2
personaslist, create, get, update, delete, analyze6
mediagenerate, get status2
connectionslist connected accounts1
API keysget current key, list, create, rename, revoke5
webhookslist, create, get, update, delete, rotate secret, list deliveries7
dropspaceget dropspace account status1
usageget plan, limits, and billing period1

recommended scopes for AI agents: read, write, generate. add publish if the agent should be able to publish launches directly.

for machine-readable API references, see dropspace.dev/llms.txt (plain text for LLMs) and dropspace.dev/openapi.json (OpenAPI 3.1 spec).

CLI

the dropspace CLI lets you manage launches, personas, and more from your terminal. same API, same tools as the MCP server — just a different interface.

install

npm install -g @jclvsh/dropspace

or run directly with npx: npx @jclvsh/dropspace launches list

commands

groupcommandscount
launcheslist, create, get, update, delete, publish, retry, generate, retry-content, analytics, batch-analytics, status, delete-post, delete-posts14
personaslist, create, get, update, delete, analyze6
connectionslist1
keysme, list, create, rename, revoke5
webhookslist, create, get, update, delete, rotate-secret, deliveries7
statusdropspace official account connections1
usageplan, limits, and billing period1

examples

# list your launches
dropspace launches list

# create and publish in one command
dropspace launches create -t "my launch" --platforms twitter,reddit --publish

# check posting status
dropspace launches status <id>

# get analytics
dropspace launches analytics <id>

# manage personas
dropspace personas list
dropspace personas create -n "my brand voice"

# check your API key
dropspace keys me

# table output
dropspace --format table launches list

run dropspace --help or dropspace launches --help for the full command reference.

API reference

launches

create, manage, and publish launches across multiple platforms.

GET/launches

list launches with pagination

parameters & response

query parameters

nametypedescription
pageoptional
integerpage number (default: 1)
page_sizeoptional
integer (1-100)items per page (default: 50)
statusoptional
stringfilter by launch status (draft, manual, trigger, scheduled, running, completed, partial, failed, cancelled)

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "status": "draft|manual|trigger|scheduled|running|completed|partial|failed|cancelled",
      "platforms": ["twitter", "reddit"],
      "scheduled_date": "ISO 8601 | null",
      "product_url": "string | null",
      "product_description": "string | null",
      "persona_id": "uuid | null",
      "media_mode": "images | video | null",
      "media_assets": [
        { "type": "image", "url": "https://...", "fal_request_id": "..." }
      ] | null,
      "dropspace_platforms": ["twitter"] | null,
      "user_platform_accounts": {
        "twitter": "token-uuid",
        "linkedin:personal": "token-uuid",
        "facebook:page:123456": "token-uuid"
      } | null,
      "created_at": "ISO 8601",
      "updated_at": "ISO 8601"
    }
  ],
  "pagination": { "page": 1, "page_size": 50, "total": 42, "total_pages": 1 }
}
POST/launches

create a launch with AI-generated or custom content

parameters & response

request body

nametypedescription
titlerequired
string (1-200 chars)launch title
product_descriptionoptional
string (1-2000 chars)product description - required unless custom_content or platform_contents is provided
platformsrequired
string[] (1-9)target platforms - can be omitted when dropspace_platforms or user_platform_accounts is provided (inferred from their union)
product_urloptional
URLproduct URL (scraped for context)
scheduled_dateoptional
ISO 8601schedule for future (≥ 15 min from now)
persona_idoptional
UUIDwriting style persona to use
dropspace_platformsoptional
string[]post via dropspace official accounts
user_platform_accountsoptional
objectmap of platform key → token_id (UUID). most platforms use simple keys: "twitter", "reddit", "instagram", "tiktok", "youtube". LinkedIn uses "linkedin:personal" for personal profiles or "linkedin:organization:<org_id>" for company pages. Facebook uses "facebook:page:<page_id>". multiple keys allowed (e.g. post to personal + org simultaneously)
mediaoptional
arrayinline media upload - each item is either `{ source: "url", url: "https://..." }` to fetch from a URL, or `{ source: "base64", data: "...", filename: "photo.jpg", mime_type: "image/jpeg" }` for raw data. the server uploads to storage and populates media_assets automatically. mutually exclusive with media_assets. max 10 items (9 images + 1 video). images: jpeg, png, webp, gif (5MB). videos: mp4, mov (512MB via URL, 4MB via base64). when provided, media_attach_platforms and media_mode are auto-inferred if not set
media_assetsoptional
arraypre-uploaded media objects (id, url, type, filename, size, mime_type). mutually exclusive with media
media_attach_platformsoptional
string[]platforms to attach media to (subset of platforms). auto-inferred from selected platforms when using media
media_modeoptional
"images" | "video"media mode for the launch. auto-inferred from media types when using media
platform_contentsoptional
objectpre-written content per platform - each value needs `content` (string). for Twitter, you can alternatively provide `thread` (string[], each ≤ 280 chars or ≤ 25,000 for X Premium accounts, max 6 tweets) instead of `content` - mutually exclusive with `content`. Reddit requires `title` (string, max 300 chars). Product Hunt and Hacker News support an optional `title` field (max 60 and 80 chars respectively). TikTok supports a `tiktok_settings` object with `privacy_level` (required before publishing: "PUBLIC_TO_EVERYONE", "FOLLOWER_OF_CREATOR", "MUTUAL_FOLLOW_FRIENDS", or "SELF_ONLY") and optional booleans: `allow_comments`, `allow_duet`, `allow_stitch`, `is_commercial`, `is_your_brand`, `is_branded_content`, `auto_add_music`. when provided, AI content generation is skipped. per-platform character limits are enforced: Twitter 280/tweet (25,000 for X Premium) × 6 tweets, LinkedIn 3,000, Instagram 2,200, Reddit 3,000, Facebook 3,000, TikTok 4,000, YouTube 5,000 (description, title max 100), Product Hunt 500, Hacker News 2,000, Substack 3,000. YouTube requires video and supports an optional `title` field (max 100 chars). mutually exclusive with custom_content
custom_contentoptional
string | string[]single text distributed to all selected platforms, or array of tweet strings when twitter is selected. string form is validated against the most restrictive platform limit. array form creates a twitter thread (each ≤280 chars or ≤25,000 for X Premium, max 6) and consolidates for other platforms. mutually exclusive with platform_contents. when provided, AI content generation is skipped
custom_content_reddit_titleoptional
string (max 300 chars)reddit post title - required when using custom_content with reddit in platforms
publishoptional
booleanimmediately publish after creation. returns 202 instead of 201. mutually exclusive with scheduled_date. requires both write and publish scopes (API key auth)
waitoptional
booleanwait for publishing to complete and return post URLs inline (returns 200 with posting_status). requires publish: true. response includes posting_status with per-platform results. typical wait: 10-30s for text platforms (Twitter, LinkedIn, Reddit, Facebook), up to 60s+ with Instagram or TikTok

response

201 Created
// 201 (create only)
{
  "data": {
    "id": "uuid",
    "name": "announcing our new feature",
    "status": "draft",
    "platforms": ["linkedin", "twitter", "reddit"],
    "scheduled_date": null,
    "created_at": "2026-03-25T12:00:00Z",
    "updated_at": "2026-03-25T12:00:00Z"
  }
}

// 200 (publish: true, wait: true)
{
  "data": {
    "id": "uuid",
    "name": "announcing our new feature",
    "status": "completed",
    "platforms": ["linkedin", "twitter"],
    "scheduled_date": null,
    "created_at": "2026-03-25T12:00:00Z",
    "updated_at": "2026-03-25T12:00:30Z",
    "posting_status": {
      "twitter": { "status": "success", "post_url": "https://x.com/user/status/123" },
      "linkedin": { "status": "success", "post_url": "https://linkedin.com/feed/update/..." }
    }
  }
}

errors

statuscodedescription
400SERVER_002validation error
400UPLOAD_001unsupported media type
400UPLOAD_002media file too large
400UPLOAD_003media storage upload failed
400LAUNCH_007platform requirements not met (e.g. Instagram/TikTok need media)
404SERVER_003persona not found
429LAUNCH_002plan launch limit reached
429RATE_001content generation rate limit

notes

  • the media field lets you upload images/videos inline via URL or base64 without pre-uploading to storage - the server handles upload and returns media_assets in the response
  • media and media_assets are mutually exclusive - use media for inline upload, or media_assets if you've already uploaded files to storage
  • content is generated automatically via Claude using your description, scraped website data, and persona style
  • custom_content and platform_contents are mutually exclusive - use one or the other (or neither for full AI generation)
  • product_url is optional but enhances AI generation by providing scraped website data for additional context
  • if platform_contents is provided, those platforms skip AI generation - only platforms without a truthy content field are generated
  • custom_content as a string distributes the same text to all platforms - validated against the lowest character limit. as an array (string[]), it creates a numbered twitter thread and joins tweets with double newlines for other platforms. array form requires twitter in platforms
  • partial coverage is supported: provide content for some platforms and let AI generate the rest
  • for Twitter threads, use `platform_contents.twitter.thread: ["tweet 1", "tweet 2"]` instead of `content` - each tweet ≤ 280 chars (≤ 25,000 for X Premium accounts), max 6 tweets. mutually exclusive with `content`
  • media is distributed to selected platforms with per-platform limits (Instagram/Facebook: 10, Reddit: 20, TikTok: 35)
  • Instagram requires at least one image, video, or AI-generated video. TikTok requires video or photos
  • TikTok requires `tiktok_settings.privacy_level` to be set before publishing (TikTok Content Sharing Guidelines). set it at creation time via `platform_contents.tiktok.tiktok_settings` or update it later via PATCH. branded content (`is_branded_content: true`) cannot use SELF_ONLY privacy
  • when `publish: true`, the launch is created and immediately queued for publishing - returns **202** instead of 201. if publish validation fails after creation, returns 201 with a `publish_error` field containing `{ code, message }` - retry via `POST /launches/:id/publish`
  • `publish` and `scheduled_date` are mutually exclusive - use one or the other
  • `publish: true` requires at least one posting account (`user_platform_accounts` or `dropspace_platforms`). with API key auth, both `write` and `publish` scopes are required
  • `wait: true` runs the publisher synchronously and returns 200 with `posting_status` containing per-platform results and post URLs. the request stays open until all platforms finish (typically 10-30s, up to 60s+ with Instagram/TikTok). if any platform fails, `posting_status` still shows successes - check `status` field per platform
GET/launches/:id

get a single launch with posting status

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "name": "string",
    "status": "completed",
    "platforms": ["linkedin", "twitter"],
    "platform_contents": { "twitter": { "content": "..." } },
    "media_assets": [],
    "dropspace_platforms": ["twitter"] | null,
    "user_platform_accounts": {
      "twitter": "token-uuid",
      "linkedin:personal": "token-uuid"
    } | null,
    "posting_status": {
      "twitter": { "status": "success", "post_url": "https://x.com/...", "posted_at": "ISO 8601" },
      "linkedin": { "status": "failed", "error_message": "rate limited by platform" }
    },
    "started_at": "ISO 8601",
    "completed_at": "ISO 8601"
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found
PATCH/launches/:id

update a draft/scheduled/cancelled launch

parameters & response

request body

nametypedescription
scheduled_dateoptional
ISO 8601 | nullschedule for future (≥ 15 min from now), null to unschedule
statusoptional
"draft" | "manual" | "trigger" | "scheduled" | "cancelled"update launch status
platformsoptional
string[]target platforms for this launch
nameoptional
stringlaunch name/title (max 200 chars)
product_descriptionoptional
stringproduct description (max 10,000 chars)
product_urloptional
stringproduct URL (empty string to clear)
persona_idoptional
string | nullpersona ID for content generation, null to clear
platform_contentsoptional
objectper-platform content update (deep-merged with existing) - fields you include replace the old value, omitted fields are preserved. for Twitter, you can use `thread` (string[], each ≤ 280 chars or ≤ 25,000 for X Premium, max 6) instead of `content` - mutually exclusive with `content`. Reddit `title` is optional (existing title preserved if omitted; max 300 chars if provided). Product Hunt and Hacker News support an optional `title` field (max 60 and 80 chars respectively). TikTok `tiktok_settings` can be set or updated here (deep-merged) - see POST docs for field details. per-platform character limits are enforced: Twitter 280/tweet (25,000 for X Premium) × 6 tweets, LinkedIn 3,000, Instagram 2,200, Reddit 3,000, Facebook 3,000, TikTok 4,000, YouTube 5,000 (description, title max 100), Product Hunt 500, Hacker News 2,000, Substack 3,000. YouTube `title` is optional on update (existing title preserved if omitted)
user_platform_accountsoptional
objectmap of platform key → token_id (UUID). most platforms use simple keys: "twitter", "reddit", "instagram", "tiktok", "youtube". LinkedIn uses "linkedin:personal" for personal profiles or "linkedin:organization:<org_id>" for company pages. Facebook uses "facebook:page:<page_id>". multiple keys allowed (e.g. post to personal + org simultaneously)
dropspace_platformsoptional
string[]post via dropspace official accounts
mediaoptional
arrayinline media upload - same format as create. replaces existing media. mutually exclusive with media_assets
media_assetsoptional
arraypre-uploaded media objects. replaces existing media. mutually exclusive with media
media_attach_platformsoptional
string[]platforms to attach media to. auto-inferred when using media
media_modeoptional
"images" | "video"media mode. auto-inferred when using media

errors

statuscodedescription
400UPLOAD_001unsupported media type
400UPLOAD_002media file too large
400UPLOAD_003media storage upload failed
404LAUNCH_001launch not found
409LAUNCH_003cannot update a running, completed, or partial launch

notes

  • all fields are optional but at least one must be provided
  • a running launch can only be updated to set status to cancelled
  • unrecognized fields return a 400 error
  • status is auto-derived for editable launches (draft, manual, trigger, scheduled). the server determines status based on your launch configuration: has posting accounts (user_platform_accounts or dropspace_platforms) + scheduled_date → scheduled. has posting accounts + no scheduled_date → trigger. has content + no posting accounts → manual. you cannot force "manual" on a launch with posting accounts — it will auto-transition to "trigger". to unschedule a launch, PATCH with { "scheduled_date": null } — status auto-becomes "trigger"
  • platform_contents merges per platform - existing platforms not included in the update are preserved. within a platform, fields you include replace the old value
  • for Twitter threads, use `platform_contents.twitter.thread: ["tweet 1", "tweet 2"]` instead of `content` - each tweet ≤ 280 chars (≤ 25,000 for X Premium), max 6 tweets
  • media and media_assets replace existing media entirely (no merging)
  • TikTok `tiktok_settings` is deep-merged - you can update individual fields (e.g. only `privacy_level`) without overwriting the rest
DELETE/launches/:id

delete a launch (any status except running)

parameters & response

errors

statuscodedescription
409LAUNCH_003cannot delete a launch that is currently running
404LAUNCH_001launch not found
POST/launches/:id/publish

queue launch for publishing (async)

parameters & response

response

202 Accepted
{ "data": { "message": "publish queued" } }

errors

statuscodedescription
404LAUNCH_001launch not found
422LAUNCH_007launch has no platforms configured for posting
400LAUNCH_007platform requirements not met - see errors array for details
409LAUNCH_004launch is not in a publishable state or already publishing
403AUTH_002plan restriction on own connected accounts (dropspace official accounts are always allowed)

notes

  • the launch transitions to running - poll /status or listen for launch.completed / launch.failed / post.deleted webhooks
  • when validation fails (LAUNCH_007), the response includes a `details` array listing all issues that must be fixed before publishing
  • platform validation checks: character limits (all platforms), Reddit title ≤ 300 chars, Reddit video mode needs video (thumbnail auto-generated if not provided), Reddit image mode needs images, Instagram reel needs video, Instagram carousel needs ≥ 2 images, TikTok requires privacy_level in tiktok_settings, TikTok video mode needs video, TikTok photo mode needs images, YouTube requires video, YouTube title ≤ 100 chars, YouTube description ≤ 5,000 chars
POST/launches/:id/retry

retry failed platforms only

parameters & response

response

202 Accepted
{
  "data": {
    "message": "retry queued",
    "platforms": ["reddit", "tiktok"]
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found
400LAUNCH_003no failed platforms to retry
409LAUNCH_004launch is already running
POST/launches/:id/retry-content

retry content generation for failed platforms

parameters & response

request body

nametypedescription
platformsoptional
string[]filter to specific platforms

response

200 OK
{
  "data": {
    "retried": ["twitter", "reddit"],
    "succeeded": ["twitter"],
    "still_failing": ["reddit"],
    "rate_limited": []
  }
}

errors

statuscodedescription
400SERVER_002no failed platforms to retry
404LAUNCH_001launch not found
429LAUNCH_002per-launch regeneration limit reached
429RATE_001content generation rate limit
POST/launches/:id/generate-content

regenerate AI content for all or specific platforms

parameters & response

request body

nametypedescription
platformsoptional
string[]platforms to regenerate (defaults to all)
generate_video_scriptsoptional
["instagram", "tiktok"] subsetgenerate video scripts for these platforms

response

200 OK
{
  "data": {
    "id": "uuid",
    "name": "announcing our new feature",
    "platform_contents": {
      "twitter": { "content": "1/ Fresh new thread..." },
      "linkedin": { "content": "New version..." }
    }
  },
  "generation": {
    "platforms_generated": ["linkedin", "twitter"],
    "failures": null
  }
}

errors

statuscodedescription
400SERVER_002no product_description or platforms
404LAUNCH_001launch not found
409LAUNCH_003launch is running/completed/partial
429RATE_001content generation rate limit
503SERVER_001content generation unavailable

notes

  • existing content for other platforms is preserved
  • media, video sources, and generated videos are never overwritten
GET/launches/:id/status

detailed posting logs per platform

parameters & response

response

200 OK
{
  "data": {
    "launch_id": "uuid",
    "launch_status": "completed",
    "posting_logs": [
      {
        "id": "uuid",
        "platform": "twitter",
        "status": "success",
        "post_url": "https://x.com/...",
        "post_id": "string | null",
        "error_message": "string | null",
        "error_code": "string | null",
        "attempt_count": 1,
        "posted_at": "ISO 8601",
        "created_at": "ISO 8601"
      }
    ]
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found
GET/launches/:id/analytics

publishing analytics with per-post engagement metrics (live refresh)

parameters & response

response

200 OK
{
  "data": {
    "launch_id": "uuid",
    "launch_name": "announcing our new feature",
    "launch_status": "completed",
    "summary": { "total": 3, "successful": 3, "failed": 0, "pending": 0 },
    "fetched_at": "2026-02-22T10:00:00Z",
    "next_refresh_at": "2026-02-22T10:05:00Z",
    "platforms": [
      {
        "platform": "twitter",
        "status": "success",
        "post_url": "https://x.com/...",
        "post_id": "1234567890",
        "posted_at": "ISO 8601",
        "cache_status": "refreshed",
        "is_deleted": false,
        "deleted_detected_at": null,
        "deletion_reason": null,
        "metrics": {
          "likes": 42,
          "retweets": 12,
          "replies": 5,
          "quotes": 2,
          "bookmarks": 8,
          "impressions": 1500,
          "urlClicks": 23,
          "profileClicks": 7,
          "fetched_at": "2026-02-22T10:00:00Z"
        }
      },
      {
        "platform": "reddit",
        "status": "success",
        "post_url": "https://reddit.com/...",
        "post_id": "abc123",
        "posted_at": "ISO 8601",
        "cache_status": "fresh",
        "is_deleted": false,
        "deleted_detected_at": null,
        "deletion_reason": null,
        "metrics": {
          "score": 156,
          "upvotes": 200,
          "upvoteRatio": 0.78,
          "comments": 34,
          "fetched_at": "2026-02-22T09:58:00Z"
        }
      }
    ]
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found

notes

  • metrics are fetched live from platform APIs when stale (older than 5 minutes). calling this endpoint triggers a refresh automatically.
  • fetched_at is the most recent timestamp when metrics were collected. next_refresh_at indicates when calling again could yield fresh data (fetched_at + 5 min).
  • cache_status per platform: fresh (< 5 min old, from cache), refreshed (fetched from platform API), stale (rate limited or no token, returning older data), unavailable (no data exists). omitted for non-success posts (failed, pending).
  • rate-limited platforms return stale cached data instead of failing - the response always includes the best available metrics.
  • metric fields vary by platform: Twitter (likes, retweets, replies, quotes, bookmarks, impressions, urlClicks, profileClicks), LinkedIn (impressions, uniqueImpressions, likes, comments, shares, clicks, engagement), Facebook (reactions, comments, shares), Instagram (views, engagement, saved, likes, comments, shares), Reddit (score, upvotes, upvoteRatio, comments), TikTok (views, likes, comments, shares)
  • is_deleted indicates whether the post was detected as removed from the platform. when true, deleted_detected_at contains the detection timestamp and deletion_reason contains the reason. possible values: not_found (post no longer exists), gone (permanently removed, HTTP 410), creator_deleted (deleted by creator), moderation_removed (removed by moderation), account_deleted (account was deleted), spam_filtered (caught by spam filter). deletion is detected during metrics refresh.
GET/launches/analytics

batch analytics for multiple launches (cache-only, no live refresh)

parameters & response

query parameters

nametypedescription
idsrequired
stringcomma-separated launch UUIDs (max 100)

response

200 OK
{
  "data": [
    {
      "launch_id": "uuid",
      "launch_name": "announcing our new feature",
      "launch_status": "completed",
      "summary": { "total": 3, "successful": 2, "failed": 1, "pending": 0 },
      "platforms": [
        {
          "platform": "twitter",
          "status": "success",
          "post_url": "https://x.com/...",
          "post_id": "1234567890",
          "posted_at": "ISO 8601",
          "is_deleted": false,
          "deleted_detected_at": null,
          "deletion_reason": null,
          "cache_status": "fresh",
          "metrics": {
            "likes": 42,
            "retweets": 12,
            "impressions": 1500,
            "fetched_at": "2026-02-22T10:00:00Z"
          }
        }
      ]
    }
  ],
  "errors": [
    { "id": "invalid-uuid", "error": { "code": "LAUNCH_001", "message": "launch not found" } }
  ]
}

errors

statuscodedescription
400SERVER_002ids parameter missing or empty
400SERVER_002more than 100 IDs provided
400SERVER_005all IDs have invalid UUID format

notes

  • returns cached analytics only - does NOT trigger a live refresh from platform APIs. use the single-launch endpoint GET /launches/:id/analytics to trigger a fresh fetch.
  • accepts up to 100 launch IDs. duplicate IDs are deduplicated. counts as 1 rate limit hit.
  • response includes partial results: valid launches in data[], invalid/not-found in errors[].
  • cache_status per platform: fresh (< 5 min old), stale (older cached data), unavailable (no metrics exist). never returns refreshed (no live fetch). omitted for non-success posts (failed, pending).
  • metric fields vary by platform - same as single-launch analytics endpoint.
DELETE/launches/:id/posts/:logId

delete a single published post from its platform

parameters & response

response

200 OK
{
  "data": {
    "success": true,
    "platform": "twitter",
    "post_id": "1234567890",
    "deleted_at": "ISO 8601"
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found
404SERVER_003posting log not found or not deletable
200DELETE_NOT_SUPPORTEDplatform does not support API deletion (Instagram, TikTok) - returned in response body, not as HTTP error

notes

  • requires 'delete' scope on your API key (not included by default - add it in API key settings)
  • only works for Twitter, Facebook, LinkedIn, and Reddit - Instagram and TikTok do not support API deletion
  • the posting log must have status 'success' and a valid post_id
  • if the post was already deleted on the platform (404), it is treated as a successful deletion
  • on success, the posting log status is updated to 'deleted'
  • logId is the posting_log UUID from the /status endpoint
DELETE/launches/:id/posts

delete all published posts for a launch from their platforms

parameters & response

response

200 OK
{
  "data": {
    "results": [
      { "success": true, "platform": "twitter", "post_id": "123", "deleted_at": "ISO 8601" },
      { "success": true, "platform": "linkedin", "post_id": "urn:li:share:456", "deleted_at": "ISO 8601" },
      { "success": false, "platform": "instagram", "post_id": "789", "error": "platform does not support deletion", "error_code": "DELETE_NOT_SUPPORTED" }
    ],
    "no_failures": false,
    "deleted_count": 2,
    "failed_count": 0,
    "skipped_count": 1
  }
}

errors

statuscodedescription
404LAUNCH_001launch not found

notes

  • requires 'delete' scope on your API key (not included by default - add it in API key settings)
  • only deletes from Twitter, Facebook, LinkedIn, and Reddit
  • Instagram and TikTok posts are skipped with DELETE_NOT_SUPPORTED (require manual deletion)
  • returns detailed results per post including success/failure status
  • skipped_count includes platforms that don't support API deletion

personas

manage AI writing personas for content generation.

GET/personas

list personas with pagination

parameters & response

query parameters

nametypedescription
pageoptional
integerpage number (default: 1)
page_sizeoptional
integer (1-100)items per page (default: 50)

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "persona_analysis": "object | null",
      "build_status": "idle|building|complete|error",
      "build_progress": 0,
      "build_started_at": "ISO 8601 | null",
      "build_error": "string | null",
      "last_analyzed_at": "ISO 8601 | null",
      "created_at": "ISO 8601",
      "updated_at": "ISO 8601"
    }
  ],
  "pagination": { "page": 1, "page_size": 50, "total": 3, "total_pages": 1 }
}
POST/personas

create a new persona

parameters & response

request body

nametypedescription
namerequired
string (1-100 chars)persona name

errors

statuscodedescription
409SERVER_009duplicate persona name
429PERSONA_001persona creation limit reached
GET/personas/:id

get persona with all writing samples

parameters & response

errors

statuscodedescription
404PERSONA_002persona not found

notes

  • includes persona_analysis, persona_analysis_structured, custom_samples, twitter_samples, reddit_samples, facebook_samples, instagram_samples, tiktok_samples, linkedin_samples, youtube_samples
PATCH/personas/:id

update name and/or writing samples

parameters & response

request body

nametypedescription
nameoptional
string (1-100 chars)persona name
custom_samplesoptional
array (max 50)custom writing samples
twitter_samplesoptional
array (max 50)Twitter writing samples
reddit_samplesoptional
array (max 50)Reddit writing samples
facebook_samplesoptional
array (max 50)Facebook writing samples
instagram_samplesoptional
array (max 50)Instagram writing samples
tiktok_samplesoptional
array (max 50)TikTok writing samples
linkedin_samplesoptional
array (max 50)LinkedIn writing samples
youtube_samplesoptional
array (max 50)YouTube writing samples

errors

statuscodedescription
404PERSONA_002persona not found
409SERVER_009duplicate persona name
DELETE/personas/:id

delete a persona

parameters & response

errors

statuscodedescription
404PERSONA_002persona not found
422SERVER_005persona in use by launches

notes

  • cannot delete if used by any launches
POST/personas/:id/analyze

trigger AI persona analysis (async)

parameters & response

request body

nametypedescription
platformsoptional
string[]which platforms to analyze
include_custom_samplesoptional
booleaninclude custom samples in analysis (default: false)

response

202 Accepted
{ "data": { "started": true, "persona_id": "uuid" } }

errors

statuscodedescription
404PERSONA_002persona not found
409SERVER_009already building
429PERSONA_003persona build limit reached

notes

  • listen for persona.analyzed webhook when complete

connections

view your OAuth platform connections (read-only).

GET/connections

list your OAuth platform connections

parameters & response

query parameters

nametypedescription
pageoptional
integerpage number (default: 1)
page_sizeoptional
integer (1-100)items per page (default: 50)

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "platform": "twitter",
      "entity_id": "string",
      "account_info": { "username": "...", "display_name": "..." },
      "account_type": "personal",
      "is_active": true,
      "expires_at": "ISO 8601 | null",
      "created_at": "ISO 8601",
      "updated_at": "ISO 8601"
    }
  ],
  "pagination": { "page": 1, "page_size": 50, "total": 5, "total_pages": 1 }
}

notes

  • connections are managed via the dashboard OAuth flow - this endpoint is read-only

dropspace

check which official dropspace accounts are connected and available for posting.

GET/dropspace/status

check which official dropspace accounts are connected

parameters & response

response

200 OK
{
  "data": {
    "platforms": [
      { "platform": "facebook", "connected": true, "account_name": "dropspace" },
      { "platform": "linkedin", "connected": true, "account_name": "dropspace" },
      { "platform": "twitter", "connected": true, "account_name": "@dropspace" },
      { "platform": "reddit", "connected": false },
      { "platform": "instagram", "connected": true, "account_name": "@dropspace" },
      { "platform": "tiktok", "connected": false },
      { "platform": "youtube", "connected": false }
    ],
    "connected_platforms": ["facebook", "linkedin", "twitter", "instagram"],
    "timestamp": "ISO 8601"
  }
}

notes

  • always returns all 7 auto-post platforms in canonical order
  • account_name is only present when connected and account info exists
  • connected_platforms lists valid values for the dropspace_platforms field when creating launches
  • checks is_active flag and token expiry from the database - does not perform live health checks

API keys

manage your API keys for authentication.

GET/keys/me

get the current API key's info

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "name": "my integration",
    "key_prefix": "ds_live_abc...",
    "scopes": ["read", "write", "publish", "generate"],
    "created_at": "ISO 8601"
  }
}

notes

  • no scope required - any valid API key can check its own permissions
GET/keys

list all API keys

parameters & response

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "name": "my integration",
      "key_prefix": "ds_live_abc...",
      "scopes": ["read", "write", "publish", "generate"],
      "last_used_at": "ISO 8601 | null",
      "revoked_at": "ISO 8601 | null",
      "created_at": "ISO 8601"
    }
  ]
}
POST/keys

create a new API key (max 10)

parameters & response

request body

nametypedescription
namerequired
string (1-100 chars)key name
scopesoptional
string[]permission scopes (default: read, write, publish, generate). available: read, write, delete, publish, generate, admin

response

201 Created
{
  "data": {
    "key": "ds_live_abc123...",
    "api_key": {
      "id": "uuid",
      "name": "my integration",
      "key_prefix": "ds_live_abc...",
      "scopes": ["read", "write", "publish", "generate"],
      "created_at": "ISO 8601"
    }
  }
}

notes

  • the full key is shown only once - store it securely
PATCH/keys/:id

rename an API key

parameters & response

request body

nametypedescription
namerequired
string (1-100 chars)new key name

response

200 OK
{
  "data": {
    "id": "uuid",
    "name": "renamed key",
    "key_prefix": "ds_live_abc...",
    "last_used_at": "ISO 8601 | null",
    "revoked_at": "ISO 8601 | null",
    "created_at": "ISO 8601"
  }
}

errors

statuscodedescription
404SERVER_003api key not found
409SERVER_009an api key with this name already exists
DELETE/keys/:id

revoke an API key

parameters & response

errors

statuscodedescription
404SERVER_003api key not found

webhooks

manage webhook endpoints for event notifications.

GET/webhooks

list webhook endpoints

parameters & response

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "url": "https://your-app.com/webhooks",
      "events": ["launch.completed", "launch.failed", "post.deleted"],
      "active": true,
      "created_at": "ISO 8601",
      "updated_at": "ISO 8601"
    }
  ]
}
POST/webhooks

create a webhook endpoint (max 10)

parameters & response

request body

nametypedescription
urlrequired
HTTPS URLwebhook delivery URL
eventsrequired
string[]event types to subscribe to

response

201 Created
{
  "data": {
    "id": "uuid",
    "url": "https://your-app.com/webhooks",
    "events": ["launch.completed", "launch.failed", "post.deleted"],
    "secret": "a1b2c3d4...",
    "active": true
  }
}

errors

statuscodedescription
400SERVER_002invalid webhook url or events
400SERVER_002maximum 10 webhook endpoints allowed

notes

  • the secret is shown only once - store it securely for signature verification
  • available events: "launch.completed" (launch finished publishing), "launch.failed" (launch failed to publish), "launch.partial" (launch partially succeeded), "media.ready" (media generation completed), "persona.analyzed" (persona analysis completed), "post.deleted" (a published post was detected as deleted from its platform)
  • post.deleted payload: { "event": "post.deleted", "data": { "launch_id": "uuid", "platform": "twitter", "post_url": "https://...", "post_id": "12345", "deletion_reason": "not_found", "detected_at": "2025-01-15T10:30:00Z" } }
  • deletion_reason values: not_found, gone, creator_deleted, moderation_removed, account_deleted, spam_filtered
GET/webhooks/:id

get a webhook endpoint

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "url": "https://your-app.com/webhooks",
    "events": ["launch.completed", "launch.failed", "post.deleted"],
    "active": true,
    "created_at": "ISO 8601",
    "updated_at": "ISO 8601"
  }
}

errors

statuscodedescription
404SERVER_003webhook not found
PATCH/webhooks/:id

update url, events, or active status

parameters & response

request body

nametypedescription
urloptional
HTTPS URLnew webhook URL
eventsoptional
string[]new event subscriptions
activeoptional
booleanenable/disable endpoint

errors

statuscodedescription
404SERVER_003webhook not found
400SERVER_002invalid webhook url or events
DELETE/webhooks/:id

delete a webhook endpoint

parameters & response

errors

statuscodedescription
404SERVER_003webhook not found
POST/webhooks/:id/rotate-secret

rotate the signing secret (new secret shown once)

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "secret": "a1b2c3d4..."
  }
}

errors

statuscodedescription
404SERVER_003webhook not found

notes

  • the new secret is shown only once - update your verification code immediately
  • the old secret becomes invalid immediately after rotation
  • in-flight webhook deliveries (already queued) use the secret from enqueue time and are not affected
GET/webhooks/:id/deliveries

list delivery attempts with pagination

parameters & response

query parameters

nametypedescription
pageoptional
integerpage number (default: 1)
page_sizeoptional
integer (1-100)items per page (default: 50)

response

200 OK
{
  "data": [
    {
      "id": "uuid",
      "event_type": "launch.completed",
      "status": "delivered|pending|failed",
      "attempts": 1,
      "response_status": 200,
      "delivered_at": "ISO 8601",
      "created_at": "ISO 8601"
    }
  ],
  "pagination": { "page": 1, "page_size": 50, "total": 12, "total_pages": 1 }
}

errors

statuscodedescription
404SERVER_003webhook not found

usage

check your current plan, billing period, and usage limits.

GET/usage

get plan limits and current usage

parameters & response

response

200 OK
{
  "data": {
    "plan": "starter",
    "billing_period": {
      "start": "ISO 8601",
      "end": "ISO 8601"
    },
    "limits": {
      "launches_per_month": { "limit": 50, "used": 3, "remaining": 47 },
      "ai_images_per_month": { "limit": 100, "used": 12, "remaining": 88 },
      "ai_videos_per_month": { "limit": 20, "used": 5, "remaining": 15 },
      "personas": { "limit": 10, "used": 2, "remaining": 8 },
      "analyses_per_persona": { "limit": 3 },
      "regenerations_per_launch": { "limit": 5 }
    },
    "features": {
      "can_connect_own_accounts": true,
      "can_post_to_official_accounts": true,
      "allowed_platforms": ["facebook", "linkedin", "twitter", "reddit", "instagram", "tiktok", "youtube"]
    }
  }
}

notes

  • limit and remaining can be "unlimited" (string) instead of a number for higher-tier plans
  • personas is a lifetime limit (not per billing period)
  • analyses_per_persona and regenerations_per_launch are per-resource limits (no used/remaining tracking)

credits

purchase and manage credit packs for MPP (Stripe) agents. credits let agents prepay for launches in bulk, reducing per-transaction fees.

GET/credits

get credit balance and available packs

parameters & response

response

200 OK
{
  "data": {
    "balance": 7,
    "packs": [
      { "id": "pack_20", "credits": 20, "price_usd": "12.00" }
    ],
    "purchase_endpoint": "POST /v1/credits/purchase"
  }
}

notes

  • returns balance 0 for x402 wallets and API key users (credits are MPP-only)
  • balance is cached in Redis (5-min TTL) with database as source of truth
POST/credits/purchase

purchase a credit pack via MPP payment

parameters & response

request body

nametypedescription
packoptional
stringcredit pack ID to purchase (default: "pack_20") (default: "pack_20")

response

200 OK
{
  "data": {
    "credits_added": 20,
    "new_balance": 27,
    "pack": { "id": "pack_20", "credits": 20, "price_usd": "12.00" }
  }
}

errors

statuscodedescription
400SERVER_002x402 wallets cannot purchase credits (MPP only)
402PAYMENT_001MPP payment required - credential missing or expired
500PAYMENT_004payment processed but credits could not be added (contact support)

notes

  • MPP agents only - x402 wallets are rejected with 400 (crypto has low fees already)
  • the MPP credential must be for the pack price ($12.00 = 1200 cents), not the per-launch price
  • credits never expire and are deducted automatically when creating launches
  • if a launch creation fails after credit deduction, the credit is refunded automatically
  • duplicate purchases with the same MPP credential are rejected (idempotent)

webhook delivery

event types

eventfired when
launch.completedall platforms posted successfully
launch.failedall platforms failed
launch.partialsome platforms succeeded, some failed
media.readyimage or video generation completed
persona.analyzedAI persona analysis finished

payload format

every delivery wraps the event-specific data in an envelope:

{
  "id": "evt_a1b2c3d4e5f6...",
  "event": "launch.completed",
  "created_at": "2026-02-18T12:00:00.000Z",
  "data": { ... }
}

event payloads

launch.completed / launch.failed / launch.partial

{
  "launch_id": "uuid",
  "launch_name": "announcing our new feature",
  "platforms": [
    { "platform": "twitter", "status": "success", "post_url": "https://x.com/..." },
    { "platform": "reddit", "status": "failed", "error_message": "rate limited" }
  ]
}

media.ready

{
  "generation_type": "image",
  "launch_id": "uuid",
  "platform": "instagram"
}

persona.analyzed

{
  "persona_id": "uuid",
  "persona_name": "my brand voice",
  "samples_analyzed": 42
}

headers

headerdescription
Content-Typeapplication/json
X-Dropspace-SignatureHMAC-SHA256 hex digest of the JSON body
X-Dropspace-Eventevent type (e.g. launch.completed)
X-Dropspace-Deliveryunique delivery ID for idempotency

signature verification

verify the X-Dropspace-Signature header using your webhook secret to ensure the request is from dropspace:

import crypto from "crypto";

function verifySignature(secret, body, signature) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// in your handler:
app.post("/webhooks", (req, res) => {
  const sig = req.headers["x-dropspace-signature"];
  if (!verifySignature(WEBHOOK_SECRET, req.rawBody, sig)) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse(req.rawBody);
  const deliveryId = req.headers["x-dropspace-delivery"];
  // use deliveryId for idempotency
  console.log(`received ${event.event}`);
  res.status(200).json({ received: true });
});

retry behavior

  • up to 3 retries with exponential backoff (via QStash)
  • 30-second timeout per delivery attempt
  • return 2xxto acknowledge — any other status triggers retry
  • use the X-Dropspace-Delivery header as an idempotency key to avoid processing the same event twice

agent payments

autonomous agents can use the dropspace API without accounts or subscriptions via two payment protocols: x402 (crypto) and MPP (Stripe).

agent payments are only supported on launch endpoints. all other endpoints require API key authentication.

how it works

  • x402 wallets get 5 free launches/month using official dropspace accounts
  • beyond the free tier, each launch creation costs $0.55 via x402 (crypto) or $0.60 via MPP (card). publishing is always free
  • payment proofs are verified for agent identification; settlement only occurs when the free tier is exceeded
  • agent users can only post to official dropspace accounts (not connected user accounts)

protocols

x402 (crypto)MPP (Stripe)
headerX-PAYMENT: <base64>Authorization: Payment <base64url>
payment methodUSDC on BaseStripe card charge
settlementon-chain transferStripe charge
402 challengeJSON body x402 objectWWW-Authenticate: Payment header
identitywallet addresspayer identity from credential

rate limits

agent-authenticated requests are rate-limited to 30 requests/minute per agent identity. for x402, identity is the wallet address. for MPP, identity is the payer credential source. exceeding this limit returns a 429 with a Retry-After header.

supported endpoints

endpointpayment required
POST /launchesafter 5 free launches/month for x402 ($0.55 x402 / $0.60 MPP)
GET /launchesfree
GET /launches/:idfree
PATCH /launches/:idfree
DELETE /launches/:idfree
POST /launches/:id/publishfree (paid at creation, not publish)
GET /launches/:id/statusfree

example flow

1. request with no payment proof

curl -X POST https://api.dropspace.dev/launches \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Product Launch",
    "product_description": "An amazing product",
    "platforms": ["twitter", "reddit"]
  }'

2. 402 response (includes both challenges)

the response body contains both x402 and mpp challenge objects. for MPP, a WWW-Authenticate: Payment <challenge> response header is also included.

{
  "error": {
    "code": "PAYMENT_001",
    "message": "payment required: $0.55 (x402) or $0.60 (MPP) per launch x402 wallets get 5 free launches/month"
  },
  "x402": {
    "version": 2,
    "scheme": "exact",
    "network": "eip155:8453",
    "asset": "USDC",
    "amount": "0.50",
    "receiver": "0x...",
    "resource": "POST /launches"
  },
  "mpp": {
    "version": 1,
    "method": "stripe",
    "intent": "charge",
    "currency": "usd",
    "amount_cents": 50,
    "resource": "POST /launches"
  }
}

3a. pay with x402 (crypto)

curl -X POST https://api.dropspace.dev/launches \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-encoded-payment-proof>" \
  -d '{
    "title": "My Product Launch",
    "product_description": "An amazing product",
    "platforms": ["twitter", "reddit"]
  }'

3b. pay with MPP (Stripe)

curl -X POST https://api.dropspace.dev/launches \
  -H "Content-Type: application/json" \
  -H "Authorization: Payment <base64url-encoded-credential>" \
  -d '{
    "title": "My Product Launch",
    "product_description": "An amazing product",
    "platforms": ["twitter", "reddit"]
  }'

credit packs

MPP (Stripe) agents can purchase credit packs to prepay for launches in bulk. instead of paying $0.60 per launch individually, buy 20 credits for $12.00 in a single Stripe charge. credits are deducted automatically when creating launches - no payment credential needed for each request.

credit packs are for MPP (Stripe) agents only. x402 (crypto) agents already have low transaction fees on Base and use per-launch USDC settlement.

pricing

packcreditspriceper launch
pack_2020$12.00$0.60

how it works

  • check balance - GET /v1/credits returns your current credit balance and available packs
  • purchase - POST /v1/credits/purchase with an MPP credential for $12.00 adds 20 credits
  • auto-deduction - when you create a launch with credits available, 1 credit is deducted instead of charging Stripe
  • out of credits - if credits run out, the next launch returns 402 with purchase instructions. buy another pack to continue
  • refunds - if launch creation fails after credit deduction, the credit is refunded automatically

purchase example

send an MPP credential for $12.00 (1200 cents) to the purchase endpoint:

curl -X POST https://api.dropspace.dev/credits/purchase \
  -H "Content-Type: application/json" \
  -H "Authorization: Payment <base64url-credential-for-1200-cents>" \
  -d '{ "pack": "pack_20" }'

# response:
# {
#   "data": {
#     "credits_added": 20,
#     "new_balance": 20,
#     "pack": { "id": "pack_20", "credits": 20, "price_usd": "12.00" }
#   }
# }

check balance

curl https://api.dropspace.dev/credits \
  -H "Authorization: Payment <base64url-credential>"

# response:
# {
#   "data": {
#     "balance": 7,
#     "packs": [{ "id": "pack_20", "credits": 20, "price_usd": "12.00" }],
#     "purchase_endpoint": "POST /v1/credits/purchase"
#   }
# }

error handling

error response shape

{
  "error": {
    "code": "LAUNCH_001",
    "message": "launch not found"
  }
}

error codes

authentication

codeHTTPmeaning
AUTH_001401invalid or revoked API key
AUTH_002403plan restriction (feature not available)

launches

codeHTTPmeaning
LAUNCH_001404launch not found
LAUNCH_002429launch or regeneration limit exceeded
LAUNCH_003409invalid launch status for operation
LAUNCH_004409launch cannot be published from current status
LAUNCH_007400platform requirements not met

personas

codeHTTPmeaning
PERSONA_001429persona creation limit reached
PERSONA_002404persona not found
PERSONA_003429persona build limit reached

media

codeHTTPmeaning
MEDIA_001429monthly media generation limit reached

uploads

codeHTTPmeaning
UPLOAD_001400unsupported media type (allowed: jpeg, png, webp, gif, mp4, mov)
UPLOAD_002400media file too large (images: 5MB, videos: 512MB URL / 4MB base64)
UPLOAD_003400media storage upload failed

payment (x402 / mpp)

codeHTTPmeaning
PAYMENT_001402payment required (free tier exceeded)
PAYMENT_002402payment verification failed
PAYMENT_003402MPP credential verification failed

rate limiting

codeHTTPmeaning
RATE_001429too many requests

validation & server

codeHTTPmeaning
SERVER_001500internal server error
SERVER_002400validation error
SERVER_003404resource not found
SERVER_004405method not allowed
SERVER_005400invalid input format or business rule violation
SERVER_008500database error
SERVER_009409conflict (e.g. status transition race)