Syndroo docs
Website GitHub

HTTP API

The 0.2.0-rc.1 candidate is one Bearer-authenticated HTTP API. It accepts immediate or scheduled posts, returns stored status, and reports per-platform outcomes. There is no dashboard in this candidate.

Base URL and headers

Every path below is relative to your deployed Worker URL, for example https://your-worker.your-subdomain.workers.dev. When you run locally the base URL is http://localhost:8787.

HeaderRequiredNotes
AuthorizationOn every /v1/ requestBearer <SYNDROO_API_KEY>. The health endpoint does not require it.
Content-TypeOn POSTMust be application/json. Anything else returns 415.
Idempotency-KeyRecommended on POSTOptional. 1 to 128 letters, digits, dots, underscores, colons, or hyphens.

Authentication

The Worker compares the supplied Bearer token against SYNDROO_API_KEY using a constant-time digest comparison. A missing or incorrect token returns HTTP 401 with code UNAUTHORIZED.

Treat the API key like a password.

Anyone who has it can publish through your Worker. Do not commit it, and do not put it in client-side code that other people can read.

Create a post

POST /v1/posts
Authorization: Bearer $SYNDROO_API_KEY
Content-Type: application/json
Idempotency-Key: example-post-001

Syndroo accepts the request before the Queue finishes publishing, so a new post returns HTTP 202 with status set to queued or scheduled. queued means accepted for processing, not yet confirmed by any platform.

Request body fields

FieldTypeRequiredNotes
contentstringYesThe shared, non-empty post text. The API caps content at 10,000 code points; individual platform limits are tighter and can fail the publication later.
platformsstring[]YesA non-empty array without duplicates. Supported names are threads, bluesky, x, tumblr, and linkedin. Each selected platform must be configured, or the request fails with 422.
overridesobjectNoPer-platform text. Each key must be one of the selected platforms, and each value takes a single content string, for example {"bluesky": {"content": "Text for Bluesky"}}.
scheduledAtstringNoAn ISO date-time with an explicit timezone, preferably UTC with Z. A future value creates status scheduled; a past value is handled as an immediate post.

Example request

curl -X POST "$SYNDROO_URL/v1/posts" \
  -H "Authorization: Bearer $SYNDROO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-post-001" \
  --data '{
    "content": "Hello from Syndroo on Threads",
    "platforms": ["threads", "bluesky"],
    "overrides": {
      "bluesky": { "content": "Hello from Syndroo on Bluesky" }
    }
  }'

This command publishes a real post once those platform credentials are configured.

Create responses

Immediate post, HTTP 202:

{
  "id": "post_...",
  "status": "queued"
}

Scheduled post, HTTP 202:

{
  "id": "post_...",
  "status": "scheduled",
  "scheduledAt": "2030-01-02T03:04:05.000Z"
}

Idempotent replay of an identical request, HTTP 200:

{
  "id": "post_...",
  "status": "queued",
  "replayed": true
}

Idempotency-Key

Idempotency-Key is optional but recommended for deployment automation. Use a stable key for one logical post.

  • Repeating the same request with the same key returns the original post with replayed: true and HTTP 200.
  • Reusing the key with different content returns HTTP 409 and code IDEMPOTENCY_CONFLICT.
  • A malformed key, such as one containing spaces or more than 128 characters, returns HTTP 400 and code INVALID_REQUEST.
  • While maintenance mode is enabled, an authenticated POST is rejected before the key is read, so nothing is recorded.

Read Idempotency for the delivery reasoning behind this header.

Check a post

curl \
  -H "Authorization: Bearer $SYNDROO_API_KEY" \
  "$SYNDROO_URL/v1/posts/post_..."

A found post returns HTTP 200. An unknown id returns HTTP 404 with code POST_NOT_FOUND.

{
  "id": "post_...",
  "content": "Hello from Syndroo",
  "platforms": ["bluesky"],
  "status": "published",
  "createdAt": "2030-01-02T03:04:05.000Z",
  "publications": [
    {
      "id": "pub_...",
      "postId": "post_...",
      "platform": "bluesky",
      "provider": "bluesky-native",
      "content": "Hello from Syndroo",
      "status": "published",
      "attempts": 1,
      "externalId": "bafyre...",
      "externalUrl": "https://bsky.app/profile/.../post/...",
      "errorAmbiguous": false,
      "createdAt": "2030-01-02T03:04:05.000Z",
      "publishedAt": "2030-01-02T03:04:06.000Z"
    }
  ]
}

If the post is still queued or publishing, wait briefly and request the same URL again. If it is failed, inspect errorCode, errorMessage, and errorAmbiguous in the publication entry.

Post object

FieldTypeNotes
idstringIdentifier such as post_....
contentstringThe shared content that was accepted.
platformsstring[]The selected platform names.
statusstringAggregate post status. See Status values.
createdAtstringISO timestamp for acceptance.
scheduledAtstringPresent when a schedule was requested.
overridesobjectPresent when per-platform content was supplied.
publicationsarrayOne entry per selected platform.

Publication object

FieldTypeNotes
idstringIdentifier such as pub_....
postIdstringThe parent post id.
platformstringOne of the selected platform names.
providerstringImplementation identifier: bluesky-native, threads-native, x-sdk, tumblr-native, or linkedin-native.
contentstringThe content sent to this platform, after any override.
statusstringPer-platform delivery status.
attemptsnumberApplication attempts so far. The maximum is three.
externalIdstringPresent after a confirmed platform success.
externalUrlstringPresent when the platform returns a usable post URL.
errorCodestringPresent on a stored failure.
errorMessagestringPresent on a stored failure.
errorAmbiguousbooleantrue when the platform may have accepted the content but Syndroo cannot confirm it.
createdAtstringISO timestamp for creation.
publishedAtstringPresent after a confirmed success.

The internal retry_at deadline is not part of the public post response.

List recent posts

curl \
  -H "Authorization: Bearer $SYNDROO_API_KEY" \
  "$SYNDROO_URL/v1/posts?limit=50"

limit is optional, defaults to 50, and must be an integer from 1 to 100. Anything else returns HTTP 400 with code INVALID_REQUEST.

{
  "items": [
    {
      "id": "post_...",
      "content": "Hello from Syndroo",
      "platforms": ["bluesky"],
      "status": "published",
      "createdAt": "2030-01-02T03:04:05.000Z"
    }
  ]
}

Status values

Post status describes the whole multi-platform request.
StatusMeaning
scheduledWaiting for scheduledAt.
queuedAccepted and waiting for a publication job.
publishingAt least one platform request is running.
publishedEvery selected platform succeeded.
partialSome platforms succeeded and some failed.
failedEvery selected platform failed.
Publication status describes one platform's delivery.
StatusMeaning
scheduledWaiting for its scheduled time.
pendingReady for Queue delivery.
publishingClaimed by a Queue consumer.
publishedPlatform confirmed success.
failedStopped after a terminal or exhausted failure.

An ambiguous outcome is represented as publication status: "failed" with errorAmbiguous: true, not as a separate status. See Ambiguous outcomes.

Errors

Errors use one JSON shape:

{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "platforms must be a non-empty array"
  }
}
HTTPCodeMeaning
400INVALID_REQUESTInvalid content, platforms, overrides, date, list limit, or a malformed Idempotency-Key.
400INVALID_JSONThe body is missing or is not valid JSON.
401UNAUTHORIZEDMissing or incorrect Bearer token.
404NOT_FOUNDThe route does not exist.
404POST_NOT_FOUNDThe requested post id does not exist.
409IDEMPOTENCY_CONFLICTThe idempotency key was already used with a different request.
413BODY_TOO_LARGEThe request body exceeds 64 KiB.
415UNSUPPORTED_MEDIA_TYPEThe request body is not sent as application/json.
422PLATFORM_NOT_CONFIGUREDThe requested platform adapter is not installed, or its credentials are missing.
503SERVICE_UNAVAILABLEMaintenance mode is enabled and the Worker is not accepting new posts.

Limits

AreaLimit
Request body64 KiB.
Shared content10,000 code points at the API layer.
Threads500 Unicode characters. Text only.
Bluesky300 Unicode characters and 3,000 UTF-8 bytes. HTTP(S) URLs receive link facets.
X280 weighted characters, validated with twitter-text before network access. Premium long posts are outside this version.
TumblrOne NPF text block, limited to 4,096 Unicode code points. HTML and Markdown are plain text; media, tags, drafts, and reblogs are not supported.
LinkedInAt most 3,000 UTF-16 units after little-text escaping. Nothing is truncated; an over-limit publication fails with INVALID_CONTENT before network access.
List pagelimit from 1 to 100, default 50.
RetriesMaximum three application attempts. Minimum waits are 60 seconds after the first failure and 120 seconds after the second. Ambiguous outcomes are never retried automatically.
Scheduling cadenceCron scans every 15 minutes, so a scheduled post can publish up to roughly 15 minutes after its requested time.
Longer content can be accepted by the API and fail later.

Text that passes the shared API cap can still exceed a platform's own limit. In that case the platform publication finishes as failed with INVALID_CONTENT. Prefer per-platform overrides when the same text does not fit everywhere.

Scheduling

Send scheduledAt as an ISO date-time with an explicit timezone. UTC with Z avoids ambiguity between machines.

{
  "content": "Shared fallback",
  "platforms": ["bluesky"],
  "overrides": {
    "bluesky": { "content": "Scheduled Bluesky post" }
  },
  "scheduledAt": "2030-01-02T03:04:05.000Z"
}

The accepted response returns status scheduled. Scheduling stays inside Syndroo; the platform's own queue is not used. An authenticated GET on the returned id shows the same post before and after it is due.

Maintenance mode

SYNDROO_MAINTENANCE is an optional, non-secret Worker variable. Only the exact string true enables it; false or an unset variable keeps normal operation.

  • An authenticated POST /v1/posts returns HTTP 503 with code SERVICE_UNAVAILABLE.
  • The rejection happens after Bearer authentication and before the request body, the idempotency key, or the database is read, so maintenance never creates a post and never records a key.
  • GET /health and the authenticated GET /v1/posts and GET /v1/posts/<id> queries keep working.
  • Requests without a valid Bearer token still return 401.
  • Maintenance is admission control only. It does not pause Queue consumers or the Cron Trigger, so accepted work, scheduled work, and stale-job recovery continue to run.
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Syndroo is in maintenance mode and is not accepting new posts; retry later with the same Idempotency-Key and request body"
  }
}