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.
| Header | Required | Notes |
|---|---|---|
Authorization | On every /v1/ request | Bearer <SYNDROO_API_KEY>. The health endpoint does not require it. |
Content-Type | On POST | Must be application/json. Anything else returns 415. |
Idempotency-Key | Recommended on POST | Optional. 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.
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
| Field | Type | Required | Notes |
|---|---|---|---|
content | string | Yes | The 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. |
platforms | string[] | Yes | A 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. |
overrides | object | No | Per-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"}}. |
scheduledAt | string | No | An 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: trueand HTTP200. - Reusing the key with different content returns HTTP
409and codeIDEMPOTENCY_CONFLICT. - A malformed key, such as one containing spaces or more than 128 characters, returns HTTP
400and codeINVALID_REQUEST. - While maintenance mode is enabled, an authenticated
POSTis 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
| Field | Type | Notes |
|---|---|---|
id | string | Identifier such as post_.... |
content | string | The shared content that was accepted. |
platforms | string[] | The selected platform names. |
status | string | Aggregate post status. See Status values. |
createdAt | string | ISO timestamp for acceptance. |
scheduledAt | string | Present when a schedule was requested. |
overrides | object | Present when per-platform content was supplied. |
publications | array | One entry per selected platform. |
Publication object
| Field | Type | Notes |
|---|---|---|
id | string | Identifier such as pub_.... |
postId | string | The parent post id. |
platform | string | One of the selected platform names. |
provider | string | Implementation identifier: bluesky-native, threads-native, x-sdk, tumblr-native, or linkedin-native. |
content | string | The content sent to this platform, after any override. |
status | string | Per-platform delivery status. |
attempts | number | Application attempts so far. The maximum is three. |
externalId | string | Present after a confirmed platform success. |
externalUrl | string | Present when the platform returns a usable post URL. |
errorCode | string | Present on a stored failure. |
errorMessage | string | Present on a stored failure. |
errorAmbiguous | boolean | true when the platform may have accepted the content but Syndroo cannot confirm it. |
createdAt | string | ISO timestamp for creation. |
publishedAt | string | Present 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
| Status | Meaning |
|---|---|
scheduled | Waiting for scheduledAt. |
queued | Accepted and waiting for a publication job. |
publishing | At least one platform request is running. |
published | Every selected platform succeeded. |
partial | Some platforms succeeded and some failed. |
failed | Every selected platform failed. |
| Status | Meaning |
|---|---|
scheduled | Waiting for its scheduled time. |
pending | Ready for Queue delivery. |
publishing | Claimed by a Queue consumer. |
published | Platform confirmed success. |
failed | Stopped 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"
}
}
| HTTP | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | Invalid content, platforms, overrides, date, list limit, or a malformed Idempotency-Key. |
400 | INVALID_JSON | The body is missing or is not valid JSON. |
401 | UNAUTHORIZED | Missing or incorrect Bearer token. |
404 | NOT_FOUND | The route does not exist. |
404 | POST_NOT_FOUND | The requested post id does not exist. |
409 | IDEMPOTENCY_CONFLICT | The idempotency key was already used with a different request. |
413 | BODY_TOO_LARGE | The request body exceeds 64 KiB. |
415 | UNSUPPORTED_MEDIA_TYPE | The request body is not sent as application/json. |
422 | PLATFORM_NOT_CONFIGURED | The requested platform adapter is not installed, or its credentials are missing. |
503 | SERVICE_UNAVAILABLE | Maintenance mode is enabled and the Worker is not accepting new posts. |
Limits
| Area | Limit |
|---|---|
| Request body | 64 KiB. |
| Shared content | 10,000 code points at the API layer. |
| Threads | 500 Unicode characters. Text only. |
| Bluesky | 300 Unicode characters and 3,000 UTF-8 bytes. HTTP(S) URLs receive link facets. |
| X | 280 weighted characters, validated with twitter-text before network access. Premium long posts are outside this version. |
| Tumblr | One NPF text block, limited to 4,096 Unicode code points. HTML and Markdown are plain text; media, tags, drafts, and reblogs are not supported. |
At 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 page | limit from 1 to 100, default 50. |
| Retries | Maximum 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 cadence | Cron scans every 15 minutes, so a scheduled post can publish up to roughly 15 minutes after its requested time. |
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/postsreturns HTTP503with codeSERVICE_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 /healthand the authenticatedGET /v1/postsandGET /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"
}
}