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.devall 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_KEYinvalid or revoked keys return 401 with error code AUTH_001.
rate limits
| limit | value | scope |
|---|---|---|
| general API | 60 req/min | per API key |
| content generation | 30 req/min | per user |
| media generation | 5 req/min | per user |
| video concurrency | 2 per user / 20 global | concurrent 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
| header | description |
|---|---|
| X-RateLimit-Limit | requests allowed per window |
| X-RateLimit-Remaining | requests remaining |
| X-RateLimit-Reset | Unix 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
| scope | description |
|---|---|
| read | view launches, personas, media, and connections |
| write | create and update launches and personas |
| delete | delete launches and personas |
| publish | publish and retry launches |
| generate | AI content and media generation |
| admin | manage 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 case | scopes |
|---|---|
| AI agents / MCP | read, write, generate |
| CI/CD publishing | read, publish |
| monitoring / dashboards | read |
| full access (admin tools) | all scopes |
audit logging
destructive and sensitive operations performed via the API are automatically logged for security auditing.
audited operations
| operation | description |
|---|---|
| delete launch | permanently removes a launch |
| delete persona | permanently removes a persona |
| publish launch | publishes content to connected platforms |
| create api key | generates a new api key |
| revoke api key | permanently disables an api key |
| create webhook | registers a new webhook endpoint |
| delete webhook | removes a webhook endpoint |
| rotate webhook secret | generates 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-mcpconfigure 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
| category | tools | count |
|---|---|---|
| launches | list, create, get, update, delete, publish, retry, generate content, retry content, get analytics, get status | 11 |
| posts | delete post, delete all posts | 2 |
| personas | list, create, get, update, delete, analyze | 6 |
| media | generate, get status | 2 |
| connections | list connected accounts | 1 |
| API keys | get current key, list, create, rename, revoke | 5 |
| webhooks | list, create, get, update, delete, rotate secret, list deliveries | 7 |
| dropspace | get dropspace account status | 1 |
| usage | get plan, limits, and billing period | 1 |
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.
/launcheslist launches with pagination
parameters & response
query parameters
| name | type | description |
|---|---|---|
pageoptional | integer | page number (default: 1) |
page_sizeoptional | integer (1–100) | items per page (default: 50) |
response
{
"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 }
}/launchescreate a launch with AI-generated or custom content
parameters & response
request body
| name | type | description |
|---|---|---|
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 | URL | product URL (scraped for context) |
scheduled_dateoptional | ISO 8601 | schedule for future (≥ 15 min from now) |
persona_idoptional | UUID | writing style persona to use |
dropspace_platformsoptional | string[] | post via dropspace official accounts |
user_platform_accountsoptional | object | map 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 | array | inline 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 | array | pre-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"] subset | platforms to generate AI videos for |
platform_contentsoptional | object | pre-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
{
"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
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | validation error |
| 400 | UPLOAD_001 | unsupported media type |
| 400 | UPLOAD_002 | media file too large |
| 400 | UPLOAD_003 | media storage upload failed |
| 400 | LAUNCH_007 | platform requirements not met (e.g. Instagram/TikTok need media) |
| 404 | SERVER_003 | persona not found |
| 429 | LAUNCH_002 | plan launch limit reached |
| 429 | RATE_001 | content 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
/launches/:idget a single launch with posting status
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch not found |
/launches/:idupdate a draft/scheduled/cancelled launch
parameters & response
request body
| name | type | description |
|---|---|---|
scheduled_dateoptional | ISO 8601 | null | schedule 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 | string | launch name/title (max 200 chars) |
product_descriptionoptional | string | product description (max 10,000 chars) |
product_urloptional | string | product URL (empty string to clear) |
persona_idoptional | string | null | persona ID for content generation, null to clear |
platform_contentsoptional | object | per-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 | object | map 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 | array | inline media upload — same format as create. replaces existing media. mutually exclusive with media_assets |
media_assetsoptional | array | pre-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
| status | code | description |
|---|---|---|
| 400 | UPLOAD_001 | unsupported media type |
| 400 | UPLOAD_002 | media file too large |
| 400 | UPLOAD_003 | media storage upload failed |
| 404 | LAUNCH_001 | launch not found |
| 409 | LAUNCH_003 | cannot 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
/launches/:iddelete a launch (any status except running)
parameters & response
errors
| status | code | description |
|---|---|---|
| 409 | LAUNCH_003 | cannot delete a launch that is currently running |
| 404 | LAUNCH_001 | launch not found |
/launches/:id/publishqueue launch for publishing (async)
parameters & response
response
{ "data": { "message": "publish queued" } }errors
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch not found |
| 422 | LAUNCH_007 | launch has no platforms configured for posting |
| 400 | LAUNCH_007 | platform requirements not met — see errors array for details |
| 409 | LAUNCH_004 | launch is not in a publishable state or already publishing |
| 403 | AUTH_002 | plan 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
/launches/:id/retryretry failed platforms only
parameters & response
response
{
"data": {
"message": "retry queued",
"platforms": ["reddit", "tiktok"]
}
}errors
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch not found |
| 400 | LAUNCH_003 | no failed platforms to retry |
| 409 | LAUNCH_004 | launch is already running |
/launches/:id/retry-contentretry content generation for failed platforms
parameters & response
request body
| name | type | description |
|---|---|---|
platformsoptional | string[] | filter to specific platforms |
response
{
"data": {
"retried": ["twitter", "reddit"],
"succeeded": ["twitter"],
"still_failing": ["reddit"],
"rate_limited": []
}
}errors
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | no failed platforms to retry |
| 404 | LAUNCH_001 | launch not found |
| 429 | LAUNCH_002 | per-launch regeneration limit reached |
| 429 | RATE_001 | content generation rate limit |
/launches/:id/generate-contentregenerate AI content for all or specific platforms
parameters & response
request body
| name | type | description |
|---|---|---|
platformsoptional | string[] | platforms to regenerate (defaults to all) |
generate_video_scriptsoptional | ["instagram", "tiktok"] subset | generate video scripts for these platforms |
response
{
"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
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | no product_description or platforms |
| 404 | LAUNCH_001 | launch not found |
| 409 | LAUNCH_003 | launch is running/completed/partial |
| 429 | RATE_001 | content generation rate limit |
| 503 | SERVER_001 | content generation unavailable |
notes
- existing content for other platforms is preserved
- media, video sources, and generated videos are never overwritten
/launches/:id/statusdetailed posting logs per platform
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch not found |
/launches/:id/analyticspublishing analytics with per-post engagement metrics (live refresh)
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch 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)
/launches/:id/posts/:logIddelete a single published post from its platform
parameters & response
response
{
"data": {
"success": true,
"platform": "twitter",
"post_id": "1234567890",
"deleted_at": "ISO 8601"
}
}errors
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch not found |
| 404 | SERVER_003 | posting log not found or not deletable |
| 200 | DELETE_NOT_SUPPORTED | platform 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
/launches/:id/postsdelete all published posts for a launch from their platforms
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | LAUNCH_001 | launch 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.
/personaslist personas with pagination
parameters & response
query parameters
| name | type | description |
|---|---|---|
pageoptional | integer | page number (default: 1) |
page_sizeoptional | integer (1–100) | items per page (default: 50) |
response
{
"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 }
}/personascreate a new persona
parameters & response
request body
| name | type | description |
|---|---|---|
namerequired | string (1–100 chars) | persona name |
errors
| status | code | description |
|---|---|---|
| 409 | SERVER_009 | duplicate persona name |
| 429 | PERSONA_001 | persona creation limit reached |
/personas/:idget persona with all writing samples
parameters & response
errors
| status | code | description |
|---|---|---|
| 404 | PERSONA_002 | persona not found |
notes
- includes persona_analysis, persona_analysis_structured, custom_samples, twitter_samples, reddit_samples, facebook_samples, instagram_samples, tiktok_samples, linkedin_samples
/personas/:idupdate name and/or writing samples
parameters & response
request body
| name | type | description |
|---|---|---|
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
| status | code | description |
|---|---|---|
| 404 | PERSONA_002 | persona not found |
| 409 | SERVER_009 | duplicate persona name |
/personas/:iddelete a persona
parameters & response
errors
| status | code | description |
|---|---|---|
| 404 | PERSONA_002 | persona not found |
| 422 | SERVER_005 | persona in use by launches |
notes
- cannot delete if used by any launches
/personas/:id/analyzetrigger AI persona analysis (async)
parameters & response
request body
| name | type | description |
|---|---|---|
platformsoptional | string[] | which platforms to analyze |
include_custom_samplesoptional | boolean | include custom samples in analysis (default: false) |
response
{ "data": { "started": true, "persona_id": "uuid" } }errors
| status | code | description |
|---|---|---|
| 404 | PERSONA_002 | persona not found |
| 409 | SERVER_009 | already building |
| 429 | PERSONA_003 | persona build limit reached |
notes
- listen for persona.analyzed webhook when complete
media
generate AI images and videos for your launches.
/media/generatesubmit an image or video generation job
parameters & response
request body
| name | type | description |
|---|---|---|
typerequired | "image" | "video" | "script_video" | generation type |
launch_idrequired | UUID | associated launch |
platformrequired | string | required for script_video: "instagram" or "tiktok" |
promptoptional | string (10–2000 chars) | generation prompt (required for script_video) |
product_descriptionoptional | string | product 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 | 8 | video duration (default: 8) |
reference_image_urloptional | URL | URL of a reference image for style/composition guidance (image type only, uses edit endpoint) |
response
{
"data": {
"status": "processing",
"job_id": "uuid",
"fal_request_id": "string",
"generation_type": "image",
"usage": { "used": 3, "limit": 50, "remaining": 47 },
"plan": "pro"
}
}errors
| status | code | description |
|---|---|---|
| 403 | AUTH_002 | plan doesn't include media generation |
| 404 | LAUNCH_001 | launch not found or not owned |
| 429 | MEDIA_001 | monthly media generation limit reached |
| 429 | RATE_001 | concurrency 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
/media/:jobIdpoll media generation status
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | media job not found |
notes
- listen for media.ready webhook when status becomes completed
connections
view your OAuth platform connections (read-only).
/connectionslist your OAuth platform connections
parameters & response
query parameters
| name | type | description |
|---|---|---|
pageoptional | integer | page number (default: 1) |
page_sizeoptional | integer (1–100) | items per page (default: 50) |
response
{
"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.
/dropspace/statuscheck which official dropspace accounts are connected
parameters & response
response
{
"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.
/keys/meget the current API key's info
parameters & response
response
{
"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
/keyslist all API keys
parameters & response
response
{
"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"
}
]
}/keyscreate a new API key (max 10)
parameters & response
request body
| name | type | description |
|---|---|---|
namerequired | string (1–100 chars) | key name |
scopesoptional | string[] | permission scopes (default: read, write, publish, generate). available: read, write, delete, publish, generate, admin |
response
{
"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
/keys/:idrename an API key
parameters & response
request body
| name | type | description |
|---|---|---|
namerequired | string (1–100 chars) | new key name |
response
{
"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
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | api key not found |
| 409 | SERVER_009 | an api key with this name already exists |
/keys/:idrevoke an API key
parameters & response
errors
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | api key not found |
webhooks
manage webhook endpoints for event notifications.
/webhookslist webhook endpoints
parameters & response
response
{
"data": [
{
"id": "uuid",
"url": "https://your-app.com/webhooks",
"events": ["launch.completed", "launch.failed"],
"active": true,
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
]
}/webhookscreate a webhook endpoint (max 10)
parameters & response
request body
| name | type | description |
|---|---|---|
urlrequired | HTTPS URL | webhook delivery URL |
eventsrequired | string[] | event types to subscribe to |
response
{
"data": {
"id": "uuid",
"url": "https://your-app.com/webhooks",
"events": ["launch.completed", "launch.failed"],
"secret": "a1b2c3d4...",
"active": true
}
}errors
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | invalid webhook url or events |
| 400 | SERVER_002 | maximum 10 webhook endpoints allowed |
notes
- the secret is shown only once — store it securely for signature verification
/webhooks/:idget a webhook endpoint
parameters & response
response
{
"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
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | webhook not found |
/webhooks/:idupdate url, events, or active status
parameters & response
request body
| name | type | description |
|---|---|---|
urloptional | HTTPS URL | new webhook URL |
eventsoptional | string[] | new event subscriptions |
activeoptional | boolean | enable/disable endpoint |
errors
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | webhook not found |
| 400 | SERVER_002 | invalid webhook url or events |
/webhooks/:iddelete a webhook endpoint
parameters & response
errors
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | webhook not found |
/webhooks/:id/rotate-secretrotate the signing secret (new secret shown once)
parameters & response
response
{
"data": {
"id": "uuid",
"secret": "a1b2c3d4..."
}
}errors
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | webhook 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
/webhooks/:id/deliverieslist delivery attempts with pagination
parameters & response
query parameters
| name | type | description |
|---|---|---|
pageoptional | integer | page number (default: 1) |
page_sizeoptional | integer (1–100) | items per page (default: 50) |
response
{
"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
| status | code | description |
|---|---|---|
| 404 | SERVER_003 | webhook not found |
usage
check your current plan, billing period, and usage limits.
/usageget plan limits and current usage
parameters & response
response
{
"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
| event | fired when |
|---|---|
| launch.completed | all platforms posted successfully |
| launch.failed | all platforms failed |
| launch.partial | some platforms succeeded, some failed |
| media.ready | image or video generation completed |
| persona.analyzed | AI 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
| header | description |
|---|---|
| Content-Type | application/json |
| X-Dropspace-Signature | HMAC-SHA256 hex digest of the JSON body |
| X-Dropspace-Event | event type (e.g. launch.completed) |
| X-Dropspace-Delivery | unique 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-Deliveryheader 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.
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
| endpoint | payment required |
|---|---|
POST /launches | after 3 free launches/month ($0.50 USDC each) |
GET /launches | free |
GET /launches/:id | free |
PATCH /launches/:id | free |
DELETE /launches/:id | free |
POST /launches/:id/publish | free (paid at creation, not publish) |
GET /launches/:id/status | free |
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
| code | HTTP | meaning |
|---|---|---|
| AUTH_001 | 401 | invalid or revoked API key |
| AUTH_002 | 403 | plan restriction (feature not available) |
launches
| code | HTTP | meaning |
|---|---|---|
| LAUNCH_001 | 404 | launch not found |
| LAUNCH_002 | 429 | launch or regeneration limit exceeded |
| LAUNCH_003 | 409 | invalid launch status for operation |
| LAUNCH_004 | 409 | launch cannot be published from current status |
| LAUNCH_007 | 400 | platform requirements not met |
personas
| code | HTTP | meaning |
|---|---|---|
| PERSONA_001 | 429 | persona creation limit reached |
| PERSONA_002 | 404 | persona not found |
| PERSONA_003 | 429 | persona build limit reached |
media
| code | HTTP | meaning |
|---|---|---|
| MEDIA_001 | 429 | monthly media generation limit reached |
uploads
| code | HTTP | meaning |
|---|---|---|
| UPLOAD_001 | 400 | unsupported media type (allowed: jpeg, png, webp, gif, mp4, mov) |
| UPLOAD_002 | 400 | media file too large (images: 5MB, videos: 512MB URL / 4MB base64) |
| UPLOAD_003 | 400 | media storage upload failed |
payment (x402)
| code | HTTP | meaning |
|---|---|---|
| PAYMENT_001 | 402 | payment required (free tier exceeded) |
| PAYMENT_002 | 402 | payment verification failed |
rate limiting
| code | HTTP | meaning |
|---|---|---|
| RATE_001 | 429 | too many requests |
validation & server
| code | HTTP | meaning |
|---|---|---|
| SERVER_001 | 500 | internal server error |
| SERVER_002 | 400 | validation error |
| SERVER_003 | 404 | resource not found |
| SERVER_004 | 405 | method not allowed |
| SERVER_005 | 400 | invalid input format or business rule violation |
| SERVER_008 | 500 | database error |
| SERVER_009 | 409 | conflict (e.g. status transition race) |