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": ["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_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 — 37 tools across 9 categories, all from your editor.
install
npx -y @jclvsh/dropspaceavailable 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).
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/dropspaceor run directly with npx: npx @jclvsh/dropspace launches list
commands
| group | commands | count |
|---|---|---|
| launches | list, create, get, update, delete, publish, retry, generate, retry-content, analytics, batch-analytics, status, delete-post, delete-posts | 14 |
| personas | list, create, get, update, delete, analyze | 6 |
| connections | list | 1 |
| keys | me, list, create, rename, revoke | 5 |
| webhooks | list, create, get, update, delete, rotate-secret, deliveries | 7 |
| status | dropspace official account connections | 1 |
| usage | plan, limits, and billing period | 1 |
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 listrun dropspace --help or dropspace launches --help for the full command reference.
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) |
statusoptional | string | filter by launch status (draft, manual, trigger, scheduled, running, completed, partial, failed, cancelled) |
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_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 | 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", "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 | 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 |
platform_contentsoptional | object | pre-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 | boolean | immediately publish after creation. returns 202 instead of 201. mutually exclusive with scheduled_date. requires both write and publish scopes (API key auth) |
waitoptional | boolean | wait 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 (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
| 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 (≤ 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
/launches/:idget a single launch with posting status
parameters & response
response
{
"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
| 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 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 | object | map 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 | 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
- 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
/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 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
/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": ["linkedin", "twitter"],
"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",
"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
| 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 (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.
/launches/analyticsbatch analytics for multiple launches (cache-only, no live refresh)
parameters & response
query parameters
| name | type | description |
|---|---|---|
idsrequired | string | comma-separated launch UUIDs (max 100) |
response
{
"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
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | ids parameter missing or empty |
| 400 | SERVER_002 | more than 100 IDs provided |
| 400 | SERVER_005 | all 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.
/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, youtube_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 |
youtube_samplesoptional | array (max 50) | YouTube 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
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 },
{ "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.
/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", "post.deleted"],
"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", "post.deleted"],
"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
- 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
/webhooks/:idget a webhook endpoint
parameters & response
response
{
"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
| 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", "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.
/creditsget credit balance and available packs
parameters & response
response
{
"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
/credits/purchasepurchase a credit pack via MPP payment
parameters & response
request body
| name | type | description |
|---|---|---|
packoptional | string | credit pack ID to purchase (default: "pack_20") (default: "pack_20") |
response
{
"data": {
"credits_added": 20,
"new_balance": 27,
"pack": { "id": "pack_20", "credits": 20, "price_usd": "12.00" }
}
}errors
| status | code | description |
|---|---|---|
| 400 | SERVER_002 | x402 wallets cannot purchase credits (MPP only) |
| 402 | PAYMENT_001 | MPP payment required - credential missing or expired |
| 500 | PAYMENT_004 | payment 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
| 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 2xxto acknowledge — any other status triggers retry
- use the
X-Dropspace-Deliveryheader 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).
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) | |
|---|---|---|
| header | X-PAYMENT: <base64> | Authorization: Payment <base64url> |
| payment method | USDC on Base | Stripe card charge |
| settlement | on-chain transfer | Stripe charge |
| 402 challenge | JSON body x402 object | WWW-Authenticate: Payment header |
| identity | wallet address | payer 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
| endpoint | payment required |
|---|---|
POST /launches | after 5 free launches/month for x402 ($0.55 x402 / $0.60 MPP) |
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. 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.
pricing
| pack | credits | price | per launch |
|---|---|---|---|
| pack_20 | 20 | $12.00 | $0.60 |
how it works
- check balance -
GET /v1/creditsreturns your current credit balance and available packs - purchase -
POST /v1/credits/purchasewith 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
| 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 / mpp)
| code | HTTP | meaning |
|---|---|---|
| PAYMENT_001 | 402 | payment required (free tier exceeded) |
| PAYMENT_002 | 402 | payment verification failed |
| PAYMENT_003 | 402 | MPP credential 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) |