SDKs
Official Node.js and Python clients for the FeedHorizon API, with types, retries, pagination and webhook verification.
FeedHorizon has official SDKs for Node.js and Python. They are thin clients over the REST API and use the same names for every field. Every example on this page works the same as the matching cURL request in the API reference.
What the SDKs do for you:
- Types: requests and responses are typed, generated from /openapi.json.
- Retries: network errors, timeouts,
429and5xxare retried with backoff, honouringRetry-After. - Idempotency: every write gets an
Idempotency-Key, the same on each attempt, so a retry never creates a second post. - Pagination: one loop walks a list across all pages.
- Errors: one error class per status, carrying
code,param,detailsand the request id. - Webhooks: signature check and parsing in one call.
| Package | Source | Requires | |
|---|---|---|---|
| Node.js / TypeScript | @feedhorizon/sdk | bash-bash/feedhorizon-node | Node.js 20+, also Deno, Bun and edge runtimes. No dependencies. |
| Python | feedhorizon | bash-bash/feedhorizon-python | Python 3.10+, depends on httpx |
Both are MIT licensed.
Install
npm install @feedhorizon/sdkCreate a client
Create an API key under Dashboard → API & Apps and set it as the environment variable FEEDHORIZON_API_KEY. The client reads it from there.
import FeedHorizon from '@feedhorizon/sdk';
const client = new FeedHorizon(); // reads FEEDHORIZON_API_KEYPass the key as apiKey (Node.js) or api_key (Python) to set it in code instead. Never commit a key.
Create a post
const { data: profiles } = await client.profiles.list();
const post = await client.posts.create({
profileId: profiles[0].id,
content: 'Hello from the FeedHorizon SDK!',
platforms: ['linkedin', 'bluesky'],
scheduledFor: '2026-11-12T09:00:00+01:00',
});
console.log(post.id, post.status); // post_… scheduledMethods return the object inside data directly. Leave out the timing field for a draft, or send addToQueue or publishNow instead of scheduledFor (see Scheduling). In Python, responses are plain dicts with type hints, and parameters keep the API's names (profileId, not profile_id).
Lists and pagination
A list call returns the first page. Loop over it to walk every item across all pages; the SDK fetches the next page when it needs it.
const page = await client.posts.list({ status: 'failed', limit: 50 });
console.log(page.data.length, page.hasNextPage());
for await (const post of client.posts.list({ updatedSince: '2026-11-01T00:00:00Z' })) {
console.log(post.id, post.status);
}from is a keyword in Python, so the calendar filter of posts.list is from_ there.
Errors
Every API error is an APIError with status, code, message, param, details and the request id. Branch on code; Errors lists them all.
import { ConflictError, InvalidRequestError } from '@feedhorizon/sdk';
try {
await client.posts.update(postId, { content: 'New text' });
} catch (error) {
if (error instanceof InvalidRequestError) {
for (const d of error.details) console.log(d.code, d.param, d.message);
} else if (error instanceof ConflictError && error.code === 'post_not_editable') {
// already publishing or published
} else {
throw error;
}
}| Class | Status |
|---|---|
InvalidRequestError | 400, 422 |
AuthenticationError | 401 |
PermissionDeniedError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
RateLimitError | 429, with the wait in seconds as retryAfter / retry_after |
InternalServerError | 5xx |
APIConnectionError, APITimeoutError | No answer from the server |
Include the request id (requestId / request_id) when you contact support.
Retries and idempotency
Failed requests are retried twice by default. Pass your own idempotency key to make a write safe to repeat across process restarts, for example from a job queue:
await client.posts.create(params, { idempotencyKey: `launch-${campaignId}` });
// per client or per request
const patient = new FeedHorizon({ maxRetries: 5, timeout: 30_000 });
await client.posts.get(postId, { maxRetries: 0, timeout: 5_000 });See Idempotency for how the server treats repeated keys.
Media
// From a public URL
const image = await client.media.import('https://example.com/launch.jpg');
// From a file (up to 5 GB)
import { readFile } from 'node:fs/promises';
const video = await client.media.upload({ file: await readFile('teaser.mp4'), filename: 'teaser.mp4', mimeType: 'video/mp4' });
await client.posts.create({ profileId, content: 'The teaser is out', platforms: ['instagram'], mediaIds: [image.id, video.id], addToQueue: true });upload announces the file and uploads the bytes in one call. See Media uploads.
Webhooks
Verify the signature against the raw request body, then handle the event. In TypeScript, event.data is typed by event.event.
import express from 'express';
import { constructWebhookEvent, WebhookVerificationError } from '@feedhorizon/sdk';
app.post('/webhooks/feedhorizon', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = await constructWebhookEvent({
payload: req.body,
signature: req.get('X-Webhook-Signature'),
secret: process.env.FEEDHORIZON_WEBHOOK_SECRET!,
});
} catch (error) {
if (error instanceof WebhookVerificationError) return res.sendStatus(400);
throw error;
}
res.sendStatus(200);
if (event.event === 'post.target.failed') {
console.log(event.data.platform, event.data.error?.code);
}
});Events older than five minutes are rejected, which stops replays. See Webhooks for every event.
Async Python
AsyncFeedHorizon has the same methods as FeedHorizon, awaited:
import asyncio
from feedhorizon import AsyncFeedHorizon
async def main() -> None:
async with AsyncFeedHorizon() as client:
post = await client.posts.get("post_…")
async for failed in client.posts.list(status="failed"):
print(failed["id"])
asyncio.run(main())Resources
| Node.js | Python | Methods |
|---|---|---|
client.me() | client.me() | Organization, plan, rate limit and scopes of the key |
client.posts | client.posts | list, get, create, validate, update, delete, publish, cancel, retry, duplicate |
client.media | client.media | list, get, create, import (Python: import_url), upload, delete |
client.profiles | client.profiles | list, get, create, update, delete |
client.profiles.queue | client.profiles.queue | get, set, slots |
client.accounts | client.accounts | list, get, delete, boards, tiktokCreator (Python: tiktok_creator) |
client.platforms | client.platforms | list, get |
client.platformInvites | client.platform_invites | list, create, delete (connect links) |
client.webhooks | client.webhooks | list, get, create, update, delete, test |
client.apiKeys | client.api_keys | list, create, delete |
Configuration
| Node.js | Python | Default |
|---|---|---|
apiKey | api_key | FEEDHORIZON_API_KEY |
baseURL | base_url | FEEDHORIZON_BASE_URL, else https://feedhorizon.com/api/v1 |
timeout | timeout | 60 seconds (60000 ms in Node.js) |
maxRetries | max_retries | 2 |
defaultHeaders | default_headers | none |
fetch | http_client | global fetch / a new httpx client |
Other languages
Every endpoint, field and webhook payload is in /openapi.json (OpenAPI 3.1). Use it with any OpenAPI client generator, or call the API with plain HTTP as the cURL examples show.
Versions
The SDKs follow semantic versioning. New API fields and endpoints arrive in minor releases; see Versioning for what the API itself promises. Release notes are in each repository's CHANGELOG and GitHub releases.