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": ["twitter", "linkedin", "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 — 36 tools across 9 categories, all from your editor.

install

npx @jclvsh/dropspace-mcp

configure for Claude Code

add to your .claude/settings.json:

{
  "mcpServers": {
    "dropspace": {
      "command": "npx",
      "args": ["-y", "@jclvsh/dropspace-mcp"],
      "env": {
        "DROPSPACE_API_KEY": "ds_live_your_key_here"
      }
    }
  }
}

configure for Cursor

add to your .cursor/mcp.json:

{
  "mcpServers": {
    "dropspace": {
      "command": "npx",
      "args": ["-y", "@jclvsh/dropspace-mcp"],
      "env": {
        "DROPSPACE_API_KEY": "ds_live_your_key_here"
      }
    }
  }
}

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

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)

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_descriptionrequired
string (1–2000 chars)product description — used as context for AI generation and stored with the launch
platformsrequired
string[] (1–9)target platforms
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". 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
generate_ai_videosoptional
["instagram", "tiktok"] subsetplatforms to generate AI videos for
platform_contentsoptional
objectpre-written content per platform — each value needs `content` (string). for Twitter, you can alternatively provide `thread` (string[], each ≤ 280 chars, 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 × 6 tweets, LinkedIn 3,000, Instagram 2,200, Reddit 3,000, Facebook 3,000, TikTok 4,000, Product Hunt 500, Hacker News 2,000, Substack 3,000. 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, 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

response

201 Created
{
  "data": {
    "id": "uuid",
    "name": "announcing our new feature",
    "status": "draft",
    "platforms": ["twitter", "linkedin", "reddit"],
    "platform_contents": {
      "twitter": { "content": "1/ Tired of spending hours...", "platform": "twitter" },
      "linkedin": { "content": "Excited to share...", "platform": "linkedin" },
      "reddit": { "title": "Show r/dropspaceapp: ...", "content": "...", "platform": "reddit" },
      "tiktok": { "content": "...", "platform": "tiktok", "tiktok_settings": { "privacy_level": "PUBLIC_TO_EVERYONE", "allow_comments": true } }
    },
    "media_assets": [],
    "media_attach_platforms": ["twitter", "linkedin"]
  }
}

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, max 6 tweets. mutually exclusive with `content`
  • if a platform in generate_ai_videos already has a video_script in platform_contents, video script generation is skipped for that platform
  • media is distributed to selected platforms with per-platform limits (Instagram/Facebook: 10, Reddit: 20, TikTok: 35)
  • if generate_ai_videos is set, video scripts are generated and rendering begins asynchronously
  • 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
GET/launches/:id

get a single launch with posting status

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "name": "string",
    "status": "completed",
    "platforms": ["twitter", "linkedin"],
    "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, 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 × 6 tweets, LinkedIn 3,000, Instagram 2,200, Reddit 3,000, Facebook 3,000, TikTok 4,000, Product Hunt 500, Hacker News 2,000, Substack 3,000
user_platform_accountsoptional
objectmap of platform key → token_id (UUID). most platforms use simple keys: "twitter", "reddit", "instagram", "tiktok". 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
  • 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, 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. just `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 (own accounts or dropspace accounts)

notes

  • the launch transitions to running — poll /status or listen for launch.completed / launch.failed 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, 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
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": ["twitter", "linkedin"],
    "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",
        "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",
        "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 (just fetched from platform API), stale (rate limited or no token, returning older data), unavailable (no data exists).
  • 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)
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
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

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

media

generate AI images and videos for your launches.

POST/media/generate

submit an image or video generation job

parameters & response

request body

nametypedescription
typerequired
"image" | "video" | "script_video"generation type
launch_idrequired
UUIDassociated launch
platformrequired
stringrequired for script_video: "instagram" or "tiktok"
promptoptional
string (10–2000 chars)generation prompt (required for script_video)
product_descriptionoptional
stringproduct context
options.aspect_ratiooptional
"1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9"aspect ratio (image supports all 10; video/script_video only supports 16:9 and 9:16)
options.duration_secondsoptional
4 | 6 | 8video duration (default: 8)
reference_image_urloptional
URLURL of a reference image for style/composition guidance (image type only, uses edit endpoint)

response

202 Accepted
{
  "data": {
    "status": "processing",
    "job_id": "uuid",
    "fal_request_id": "string",
    "generation_type": "image",
    "usage": { "used": 3, "limit": 50, "remaining": 47 },
    "plan": "pro"
  }
}

errors

statuscodedescription
403AUTH_002plan doesn't include media generation
404LAUNCH_001launch not found or not owned
429MEDIA_001monthly media generation limit reached
429RATE_001concurrency limit reached

notes

  • type 'script_video' generates a video from a text script (requires platform: 'instagram' or 'tiktok' and an explicit prompt). type 'video' generates from a visual/cinematic prompt
GET/media/:jobId

poll media generation status

parameters & response

response

200 OK
{
  "data": {
    "id": "uuid",
    "generation_type": "image",
    "prompt": "...",
    "result_url": "https://cdn.dropspace.dev/...",
    "reference_image_url": "https://cdn.dropspace.dev/... | null",
    "status": "processing|completed|failed",
    "progress": 75,
    "model_id": "string",
    "error_message": "string | null",
    "launch_id": "uuid",
    "created_at": "ISO 8601",
    "completed_at": "ISO 8601 | null"
  }
}

errors

statuscodedescription
404SERVER_003media job not found

notes

  • listen for media.ready webhook when status becomes completed

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 }
    ],
    "connected_platforms": ["facebook", "linkedin", "twitter", "instagram"],
    "timestamp": "ISO 8601"
  }
}

notes

  • always returns all 6 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"],
      "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"],
    "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
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"],
    "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"]
    }
  }
}

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)

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 2xx to acknowledge — any other status triggers retry
  • use the X-Dropspace-Delivery header as an idempotency key to avoid processing the same event twice

x402 payments

autonomous agents with crypto wallets can use the dropspace API without accounts or subscriptions via the x402 payment protocol.

x402 is only supported on launch endpoints. all other endpoints require API key authentication.

how it works

  • wallet users get 3 free launches/month using official dropspace accounts
  • beyond the free tier, each launch creation costs $0.50 USDC on Base (publishing is always free)
  • payment proofs are verified for wallet identification; settlement only occurs when the free tier is exceeded
  • wallet users can only post to official dropspace accounts (not connected user accounts)

rate limits

wallet-authenticated requests are rate-limited to 30 requests/minute per wallet address. exceeding this limit returns a 429 with a Retry-After header.

supported endpoints

endpointpayment required
POST /launchesafter 3 free launches/month ($0.50 USDC each)
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. create a launch (free tier)

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"]
  }'

2. 402 response (when payment required)

{
  "error": {
    "code": "PAYMENT_001",
    "message": "payment required: $0.50 USDC per launch beyond 3 free launches/month"
  },
  "x402": {
    "version": 2,
    "scheme": "exact",
    "network": "eip155:8453",
    "asset": "USDC",
    "amount": "0.50",
    "receiver": "0x...",
    "resource": "POST /launches"
  }
}

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)

codeHTTPmeaning
PAYMENT_001402payment required (free tier exceeded)
PAYMENT_002402payment 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)