REST API v1

Developer API Reference

Create, manage, and publish social media drafts programmatically. All endpoints require an API token: generate one in your account settings.

Prefer natural language? Use DemandBird inside Claude →
Business

The REST API and MCP server are available on the Business and Agency plans. See plans & what's included.

Authentication

Every request must include your API token as a Bearer token in the Authorization header. Generate a token from Settings → API Tokens — see API tokens for how to scope and revoke one.

Authorization: Bearer YOUR_API_TOKEN
Keep your API token secret. Treat it like a password; don't commit it to source code or expose it in client-side JavaScript.

Permissions

A token can be limited to certain actions when it's created. Writing needs the “compose posts” permission, the AI endpoints need “use AI generation”, publishing needs “publish posts” and deleting needs “delete posts”. A token never has more rights than the person who made it.

A request the token isn't allowed to make returns 403:

{"success": false, "error": "You are not authorized to perform this action."}

Base URL

All API endpoints are relative to:

https://app.demandbird.com/api/v1/accounts/{account_id}

Your account_id is the numeric ID shown in your account settings. All endpoints are scoped to a single account; you cannot access another account's data even with a valid token.

Two endpoints sit outside this prefix: Write a draft with AI and Next free queue slot. They live at https://app.demandbird.com/api/v1/ and take account_id as a parameter instead.

Rate limits

1,000
requests per hour
60
requests per minute (burst)

When you exceed a limit the API returns 429 Too Many Requests with headers:

HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 1735689600

{"error": "Rate limit exceeded. Try again later."}

Wait until RateLimit-Reset (Unix timestamp) before retrying.

Errors

StatusMeaning
401 UnauthorizedMissing or invalid API token.
403 ForbiddenToken is valid but you don't have access to this account, or the token lacks the permission for this action.
404 Not FoundDraft or resource not found.
422 Unprocessable EntityValidation failed. Check the errors array in the response body.
429 Too Many RequestsRate limit exceeded.
500 Internal Server ErrorSomething went wrong on our end.

Error responses always include an error or errors key:

{"error": "Draft not found"}

{"errors": ["Content can't be blank", "Platforms can't be empty"]}

List drafts

GET/drafts

Returns a paginated list of drafts for the account. Defaults to draft and scheduled status.

Query parameters

ParameterTypeDescription
statusoptionalstringFilter by status. One of: draft, scheduled, posted, failed. Defaults to returning draft and scheduled combined.
pageoptionalintegerPage number. Defaults to 1. Returns 20 results per page.
curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/accounts/42/drafts"

# Filter by status
curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/accounts/42/drafts?status=scheduled"

Response:

{
  "drafts": [
    {
      "id": 123,
      "status": "draft",
      "content": "Big news: we just shipped X...",
      "platforms": ["linkedin", "twitter"],
      "scheduled_at": null,
      "published_at": null,
      "created_at": "2026-03-01T09:00:00Z",
      "updated_at": "2026-03-01T09:00:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "items": 20,
    "count": 42,
    "pages": 3
  }
}

Get a draft

GET/drafts/:id

Retrieve a single draft by its ID.

curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/accounts/42/drafts/123"

Response:

{
  "draft": {
    "id": 123,
    "status": "draft",
    "content": "Big news: we just shipped X...",
    "platforms": ["linkedin", "twitter"],
    "scheduled_at": null,
    "published_at": null,
    "created_at": "2026-03-01T09:00:00Z",
    "updated_at": "2026-03-01T09:00:00Z"
  }
}

Create a draft

POST/drafts

Create a new draft. The post is saved with draft status and is not published until you call the publish endpoint.

Request body (JSON)

FieldTypeDescription
contentrequiredstringThe text content of the post.
platformsoptionalarrayPlatforms to publish to. Supported values: linkedin, twitter, threads, bluesky, substack, tiktok, youtube, instagram.
scheduled_atoptionalstring (ISO 8601)Stores an intended publish time on the draft for reference. This does not schedule the post for publishing. To schedule, call publish with scheduled_at in the body.
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Big news: we just shipped X. Here is what changed...",
    "platforms": ["linkedin", "twitter"]
  }' \
  "https://app.demandbird.com/api/v1/accounts/42/drafts"

Returns 201 Created with the new draft:

{
  "draft": {
    "id": 124,
    "status": "draft",
    "content": "Big news: we just shipped X...",
    "platforms": ["linkedin", "twitter"],
    "scheduled_at": null,
    "published_at": null,
    "created_at": "2026-03-02T10:30:00Z",
    "updated_at": "2026-03-02T10:30:00Z"
  }
}

Update a draft

PATCH/drafts/:id

Update content, platforms, or scheduled time on an existing draft. All fields are optional; only send the fields you want to change.

Request body (JSON)

FieldTypeDescription
contentoptionalstringNew text content for the post.
platformsoptionalarrayReplaces the current platform selection.
scheduled_atoptionalstring (ISO 8601)Update the stored intended publish time on the draft. As with create, this stores a reference time only; it does not schedule the post.
on_variant_conflictoptionalstringAccepted for older clients and ignored.

An edit changes the shared draft only. A channel's own tailored copy is never changed or refused.

curl -X PATCH \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Updated copy, cleaner and tighter.",
    "platforms": ["linkedin"]
  }' \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/124"

Response:

{
  "draft": {
    "id": 124,
    "status": "draft",
    "content": "Updated copy, cleaner and tighter.",
    "platforms": ["linkedin"],
    ...
  }
}

Publish or schedule a draft

POST/drafts/:id/publish

Publish a draft immediately, or schedule it for a future time, to all selected platforms. The draft must have at least one platform set; update it first if needed.

With no body (or no scheduled_at), the post publishes immediately: status changes to posting, platform jobs are enqueued, and the status transitions to posted (or failed) once complete.

With scheduled_at in the body, the post is scheduled for that time instead: status becomes scheduled, no posting jobs run until the scheduled time arrives. The time must be in the future and parseable as ISO 8601.

Body parameters

ParameterTypeDescription
scheduled_atoptionalstring (ISO 8601)If present and in the future, schedules the post for that time instead of publishing immediately. Past or malformed values return 422.
platform_account_idsoptionalobjectMap of platform to connected-account id (e.g. {"twitter": 42}) when an account has multiple connections for a platform. Each id must belong to the current account and match the platform.
Immediate publishing is irreversible. Scheduled posts can still be edited or removed before their scheduled time.

Publish immediately:

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/124/publish"

Schedule for later:

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scheduled_at": "2026-04-28T09:00:00-07:00"}' \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/124/publish"

Response (immediate):

{
  "draft": {
    "id": 124,
    "status": "posting",
    "content": "Updated copy, cleaner and tighter.",
    "platforms": ["linkedin"],
    "scheduled_at": "2026-03-02T11:00:00Z",
    "published_at": null,
    "created_at": "2026-03-02T10:30:00Z",
    "updated_at": "2026-03-02T11:00:00Z"
  },
  "message": "Post is being published to linkedin"
}

Response (scheduled):

{
  "draft": {
    "id": 124,
    "status": "scheduled",
    "content": "Updated copy, cleaner and tighter.",
    "platforms": ["linkedin"],
    "scheduled_at": "2026-04-28T16:00:00Z",
    "published_at": null,
    "created_at": "2026-03-02T10:30:00Z",
    "updated_at": "2026-03-02T11:00:00Z"
  },
  "message": "Post scheduled for linkedin"
}

published_at populates once the platform confirms the post. Poll GET /drafts/:id to check the final status.

Delete a post

DELETE/drafts/:id

Remove a post by ID. Works for any status: drafts, scheduled, posted, or failed. Returns 204 No Content on success.

For posts already published to a platform: this removes the DemandBird record (and its analytics linkage); the live copy on the platform stays put. To take down the live post, delete it on the platform itself.
curl -X DELETE \
  -H "Authorization: Bearer YOUR_TOKEN" \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/124"

Empty 204 response on success. 404 if the post doesn't exist or belongs to a different account.

Write a draft with AI

POST/api/v1/compose_drafts

Writes a new draft from an idea, in the user's voice and the workspace's brand. The draft is saved, and its post_id works with every drafts endpoint (get, update, revise, publish).

Request body (JSON)

FieldTypeDescription
account_idrequiredintegerThe account to write the draft in. Not needed if the token is scoped to one account.
promptrequiredstringThe idea to write about.
platformoptionalstringlinkedin, twitter, bluesky or threads shape the length of the draft.
draft_anywayoptionalbooleanWrite the draft even if the AI would ask for more detail first.
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": 42,
    "prompt": "Why we moved our onboarding calls to async video",
    "platform": "linkedin"
  }' \
  "https://app.demandbird.com/api/v1/compose_drafts"

Returns 201 Created with the saved draft:

{
  "kind": "draft",
  "post_id": 123,
  "content": "Generated text",
  "platform": "linkedin",
  "unsupported_numbers": ["73%"]
}

unsupported_numbers lists numbers the AI wrote that weren't in your prompt, so you can check them before publishing. It's [] when there are none. platform is null if you didn't send one.

If the AI needs more to go on, it returns 200 OK with a question and saves nothing:

{
  "kind": "need_more",
  "ask": "Which audience is this for?"
}

Add the detail to prompt and send again, or send draft_anyway: true.

Errors

StatusMeaning
403The token lacks the “use AI generation” permission.
422{"error": "prompt is required"}
502{"error": "AI service error: …"}
503{"error": "AI service not configured"}

Revise a draft with AI

POST/api/v1/accounts/{account_id}/drafts/{id}/revise

Rewrites the same draft from your feedback (“make it shorter”, “less salesy”). The draft keeps its images, schedule and any per-channel copies. Use this instead of writing a new draft when you're iterating.

Only drafts with status draft or scheduled can be revised.

Request body (JSON)

FieldTypeDescription
feedbackrequiredstringWhat to change.
platformoptionalstringSets the length target only. It never changes the draft's platforms.
draft_anywayoptionalbooleanRevise even if the AI would ask for more detail first.
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"feedback": "Make it shorter and less salesy."}' \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/123/revise"

Returns 200 OK with the revised draft:

{
  "kind": "draft",
  "draft": {
    "id": 123,
    "status": "draft",
    "content": "Revised text",
    "platforms": ["linkedin"],
    "scheduled_at": null,
    "published_at": null,
    "created_at": "2026-03-02T10:30:00Z",
    "updated_at": "2026-03-02T10:45:00Z"
  },
  "unsupported_numbers": []
}

unsupported_numbers works as in Write a draft with AI. If the AI needs more detail, it returns 200 OK and leaves the draft untouched:

{
  "kind": "need_more",
  "ask": "Shorter for which audience?",
  "post_id": 123
}

Errors

StatusMeaning
403The token lacks the “use AI generation” permission.
404{"error": "Draft not found"}
422{"error": "feedback is required"}
422The draft is already posted or has no text.
422The account's AI allowance is used up.
502{"error": "AI service error: …"}
503{"error": "AI service not configured"}

Attach source material

POST/api/v1/accounts/{account_id}/drafts/{id}/references

Stores a transcript, article or notes with a draft. Reviewers can read it, and the AI writes from it when you revise the draft.

Source material is never published. Put it here, never in the draft's content, because content is what gets published.

Request body (JSON)

Send exactly one of text or url.

FieldTypeDescription
textone ofstringThe material itself.
urlone ofstringA link to the material. We fetch it for you.
titleoptionalstringA label for the material.
curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Kickoff call",
    "text": "Transcript of the kickoff call..."
  }' \
  "https://app.demandbird.com/api/v1/accounts/42/drafts/123/references"

Returns 201 Created:

{
  "reference": {
    "id": 9,
    "idea_source_id": 44,
    "title": "Kickoff call",
    "url": null,
    "kind": "paste"
  },
  "draft": {
    "id": 123,
    "status": "draft",
    "content": "Big news: we just shipped X...",
    "platforms": ["linkedin"],
    ...
  }
}

kind is "paste" for text and "url" for url.

Errors

StatusMeaning
403The token lacks the permission for this action.
404{"error": "Draft not found"}
422Both or neither of text and url were given.
422The URL couldn't be fetched.

Next free queue slot

GET/api/v1/queue_slots

Returns the next open slot in the posting queue for each platform. Pass the time to publish as scheduled_at.

Query parameters

ParameterTypeDescription
account_idintegerThe account whose queue to check.
platforms[]requiredstringA platform to check. Repeat for each platform.
platform_account_ids[<platform>]optionalintegerPicks one connected account for a platform, e.g. platform_account_ids[twitter]=7.
curl -g -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/queue_slots?account_id=42&platforms[]=linkedin&platforms[]=twitter"

Response:

{
  "slots": {
    "linkedin": "2026-09-23T09:00:00Z",
    "twitter": null
  }
}

null means no queue slots are set up for that platform.

Errors

StatusMeaning
422{"error": "platforms parameter is required"}
422None of the listed platforms is supported.

List published posts

GET/posts

Returns a paginated list of posts with posted status, ordered by most-recently-updated. Filter by date range with sinceand until.

Query parameters

ParameterTypeDescription
sinceoptionalstring (ISO 8601)Return posts updated at or after this timestamp.
untiloptionalstring (ISO 8601)Return posts updated at or before this timestamp.
pageoptionalintegerPage number. Defaults to 1.
curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/accounts/42/posts"

# Filter by date range
curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://app.demandbird.com/api/v1/accounts/42/posts?since=2026-03-01T00:00:00Z"

Response:

{
  "posts": [
    {
      "id": 121,
      "status": "posted",
      "content": "Post that went live yesterday...",
      "platforms": ["linkedin"],
      "scheduled_at": "2026-03-01T08:00:00Z",
      "published_at": "2026-03-01T08:01:32Z",
      "created_at": "2026-02-28T20:00:00Z",
      "updated_at": "2026-03-01T08:01:32Z"
    }
  ],
  "meta": {
    "page": 1,
    "items": 20,
    "count": 8,
    "pages": 1
  }
}

Draft object reference

FieldTypeDescription
idintegerUnique identifier.
statusstringdraft · scheduled · posting · posted · failed
contentstringPlain-text content of the post.
platformsarray of stringsSelected publish targets.
scheduled_atstring or nullISO 8601 timestamp if a schedule was set.
published_atstring or nullISO 8601 timestamp when the post was confirmed published. null until posted.
created_atstringISO 8601 creation timestamp.
updated_atstringISO 8601 last-updated timestamp.