Scheduling
Create drafts, schedule posts, use the queue or publish right away.
Diese Seite gibt es noch nicht auf Deutsch, deshalb siehst du sie auf Englisch.
Timing modes
POST /posts decides what to do from the timing field you send. Send at most one:
| Field | Result |
|---|---|
| none | The post is saved as a draft |
scheduledFor | Published at that time |
addToQueue: true | Takes the profile's next free queue slot |
publishNow: true | Published right away |
Schedule a post
The Node.js and Python examples use the SDKs, with a client created as shown there.
curl -X POST https://feedhorizon.com/api/v1/posts \
-H "Authorization: Bearer fh_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-post-2026-11-10" \
-d '{
"profileId": "YOUR_PROFILE_ID",
"content": "Launching tomorrow at noon",
"platforms": ["linkedin"],
"scheduledFor": "2026-11-10T12:00:00-05:00",
"timezone": "America/New_York"
}'platforms posts to every active account of those platforms in the profile. To pick accounts yourself, send accountIds instead.
Own text per account
X allows 280 characters, LinkedIn 3,000. Send targets to pick the accounts and give any of them its own text. A target without content uses the post's content.
{
"profileId": "YOUR_PROFILE_ID",
"content": "We just shipped scheduled exports. Every report can now land in your inbox on a fixed day, as CSV or PDF, without anyone pressing a button. Here is how it works and why we built it.",
"targets": [
{ "accountId": "LINKEDIN_ACCOUNT_ID" },
{ "accountId": "X_ACCOUNT_ID", "content": "Scheduled exports are live: reports as CSV or PDF, on a fixed day." }
],
"addToQueue": true
}Check before you create
Every post is checked against each platform before it is stored: text length, number and type of media, whether the platform is available, and whether a scheduled time lies in the past. If something fails, the request answers 400 and error.details lists every problem at once, each with its code and the field in param (for example targets.1.content).
POST /api/v1/posts/validate takes the same body and runs the same checks without creating anything. It answers 200 with valid, errors, warnings, the accounts the post would go to and when it would go out:
{
"data": {
"valid": false,
"errors": [
{ "code": "content_too_long", "message": "X (Twitter) (Acme) allows 280 characters, the text has 312", "param": "content", "accountId": "…", "platform": "twitter" }
],
"warnings": [],
"scheduledAt": "2026-11-10T17:00:00.000Z",
"targets": [{ "accountId": "…", "platform": "twitter" }]
}
}Warnings do not block the post. The most common one is account_not_active: the account has to be reconnected before the post goes out.
Time zones
scheduledForis an absolute ISO 8601 timestamp and must carryZor an offset such as+01:00. A time without a zone is rejected, so there is never a guess about which noon you meant.timezoneis an IANA name such asEurope/Berlin. It is stored with the post and used for display. It defaults to the profile's time zone.- All timestamps in responses are UTC.
Status lifecycle
A post has one target per social account, and each target has its own status. The post status sums them up:
| Post status | Meaning |
|---|---|
draft | Not scheduled |
scheduled | Waiting for its time |
publishing | Being sent to the platforms |
published | All targets published |
partially_failed | Some targets published, others failed |
failed | All targets failed |
A background job checks for due posts every minute.
When a platform is rate limiting or does not answer, FeedHorizon tries again: 3 attempts in total, after 2 and 10 minutes. Between attempts the target shows failed with its error, while the post stays publishing. Errors that would fail the same way again, such as a refused post or an expired connection, are not retried. Each account sends post.target.published or post.target.failed as soon as it is done; the post sends post.published, post.partial or post.failed once all accounts are done. See Publishing errors.
Retry failed posts
curl -X POST https://feedhorizon.com/api/v1/posts/POST_ID/retry \
-H "Authorization: Bearer fh_YOUR_API_KEY"Only the failed targets are published again. Targets that already went out stay as they are.
Publish, cancel, duplicate
| Call | What it does |
|---|---|
POST /posts/{id}/publish | Publishes a draft or scheduled post right away |
POST /posts/{id}/cancel | Takes a scheduled post off the schedule; it stays as a draft |
POST /posts/{id}/duplicate | Copies a post as a new draft, with its accounts, own texts and media |
The queue
Each profile has weekly posting times. addToQueue: true puts a post into the next free one.
curl -X PUT https://feedhorizon.com/api/v1/profiles/PROFILE_ID/queue \
-H "Authorization: Bearer fh_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"timezone": "Europe/Berlin",
"slots": [
{ "dayOfWeek": 1, "time": "09:00" },
{ "dayOfWeek": 3, "time": "09:00" },
{ "dayOfWeek": 5, "time": "16:30" }
]
}'dayOfWeek runs from 0 (Sunday) to 6 (Saturday), time is local time in the queue's time zone. PUT replaces all slots; send "reshuffle": true to move posts already in the queue to the new times.
GET /profiles/{id}/queue/slots?count=10 lists the next slots with the post that holds each one. Add free=true to see only the times addToQueue would pick.
Keep in sync
GET /posts?updatedSince=2026-11-04T12:00:00Z returns only posts changed since then. Store the time before each sync and pass it next time. Deleted posts do not show up there; listen for the post.deleted webhook.
Best practices
- Send an
Idempotency-Keywith everyPOST /posts, so a retry after a timeout never posts twice - Subscribe to webhooks for
post.published,post.partialandpost.failedinstead of polling - Keep rate limits in mind when scheduling in bulk