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_TOKENPermissions
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
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
| Status | Meaning |
|---|---|
| 401 Unauthorized | Missing or invalid API token. |
| 403 Forbidden | Token is valid but you don't have access to this account, or the token lacks the permission for this action. |
| 404 Not Found | Draft or resource not found. |
| 422 Unprocessable Entity | Validation failed. Check the errors array in the response body. |
| 429 Too Many Requests | Rate limit exceeded. |
| 500 Internal Server Error | Something 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
Returns a paginated list of drafts for the account. Defaults to draft and scheduled status.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| statusoptional | string | Filter by status. One of: draft, scheduled, posted, failed. Defaults to returning draft and scheduled combined. |
| pageoptional | integer | Page 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
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
Create a new draft. The post is saved with draft status and is not published until you call the publish endpoint.
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| contentrequired | string | The text content of the post. |
| platformsoptional | array | Platforms to publish to. Supported values: linkedin, twitter, threads, bluesky, substack, tiktok, youtube, instagram. |
| scheduled_atoptional | string (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
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)
| Field | Type | Description |
|---|---|---|
| contentoptional | string | New text content for the post. |
| platformsoptional | array | Replaces the current platform selection. |
| scheduled_atoptional | string (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_conflictoptional | string | Accepted 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
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
| Parameter | Type | Description |
|---|---|---|
| scheduled_atoptional | string (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_idsoptional | object | Map 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. |
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
Remove a post by ID. Works for any status: drafts, scheduled, posted, or failed. Returns 204 No Content on success.
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
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)
| Field | Type | Description |
|---|---|---|
| account_idrequired | integer | The account to write the draft in. Not needed if the token is scoped to one account. |
| promptrequired | string | The idea to write about. |
| platformoptional | string | linkedin, twitter, bluesky or threads shape the length of the draft. |
| draft_anywayoptional | boolean | Write 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
| Status | Meaning |
|---|---|
| 403 | The 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
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)
| Field | Type | Description |
|---|---|---|
| feedbackrequired | string | What to change. |
| platformoptional | string | Sets the length target only. It never changes the draft's platforms. |
| draft_anywayoptional | boolean | Revise 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
| Status | Meaning |
|---|---|
| 403 | The token lacks the “use AI generation” permission. |
| 404 | {"error": "Draft not found"} |
| 422 | {"error": "feedback is required"} |
| 422 | The draft is already posted or has no text. |
| 422 | The account's AI allowance is used up. |
| 502 | {"error": "AI service error: …"} |
| 503 | {"error": "AI service not configured"} |
Attach source material
Stores a transcript, article or notes with a draft. Reviewers can read it, and the AI writes from it when you revise the draft.
content, because content is what gets published.Request body (JSON)
Send exactly one of text or url.
| Field | Type | Description |
|---|---|---|
| textone of | string | The material itself. |
| urlone of | string | A link to the material. We fetch it for you. |
| titleoptional | string | A 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
| Status | Meaning |
|---|---|
| 403 | The token lacks the permission for this action. |
| 404 | {"error": "Draft not found"} |
| 422 | Both or neither of text and url were given. |
| 422 | The URL couldn't be fetched. |
Next free queue slot
Returns the next open slot in the posting queue for each platform. Pass the time to publish as scheduled_at.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| account_id | integer | The account whose queue to check. |
| platforms[]required | string | A platform to check. Repeat for each platform. |
| platform_account_ids[<platform>]optional | integer | Picks 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
| Status | Meaning |
|---|---|
| 422 | {"error": "platforms parameter is required"} |
| 422 | None of the listed platforms is supported. |
List published posts
Returns a paginated list of posts with posted status, ordered by most-recently-updated. Filter by date range with sinceand until.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| sinceoptional | string (ISO 8601) | Return posts updated at or after this timestamp. |
| untiloptional | string (ISO 8601) | Return posts updated at or before this timestamp. |
| pageoptional | integer | Page 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
| Field | Type | Description |
|---|---|---|
| id | integer | Unique identifier. |
| status | string | draft · scheduled · posting · posted · failed |
| content | string | Plain-text content of the post. |
| platforms | array of strings | Selected publish targets. |
| scheduled_at | string or null | ISO 8601 timestamp if a schedule was set. |
| published_at | string or null | ISO 8601 timestamp when the post was confirmed published. null until posted. |
| created_at | string | ISO 8601 creation timestamp. |
| updated_at | string | ISO 8601 last-updated timestamp. |