API policies
The machine-readable contract is openapi.json; the
worked example is quickstart.md. This page publishes
the numbers and semantics developers get burned by: rate limits, errors,
idempotency, webhook delivery, versioning.
Authentication
Authorization: Bearer <token> on every request. Sanctum personal access
tokens, created in Settings → API. No cookies, no CSRF — the API host
never accepts session authentication, and a request carrying a valid app
session cookie but no bearer token is still a 401.
Tokens carry abilities as scopes:
feeds:read feeds:writeposts:read posts:writechannels:read publishers:readaccount:readA token created with no abilities ticked gets the read abilities as explicit strings — never a wildcard. A wrong ability is a 403; a revoked, expired, or unknown token is a 401. Foreign record ids are 404, never 403, so existence does not leak across accounts. At most 20 active tokens per user.
Rate limits
Two buckets. The per-user bucket is shared by all of a user’s tokens; the per-IP bucket runs before authentication as a flood ceiling and never caps legitimate plan usage.
| Membership | Requests/min (per user) |
|---|---|
free (the fallback new users get) | 30 |
starter | 60 |
pro | 180 |
agency | 600 |
| unlimited (flag, override, or addon) | 3000 protective ceiling |
A missing or zero entitlement means no API access: 403 forbidden, never
the IP bucket. Plan changes take up to 60 s to reach the limiter. The IP
flood ceiling (6000/min) sits above every plan budget; an IP collecting
60 failed authentications in a minute is refused before token lookup.
Every authenticated response carries X-RateLimit-Limit, -Remaining
and -Reset. A 429 carries Retry-After — back off for that many
seconds. If the counter store is unavailable the API fails closed with
503 rather than bypassing limits. The MCP server (/mcp) uses the same
per-user bucket.
Errors
Error responses use one envelope:
{ "error": { "type": "validation_failed", "message": "The given data was invalid.", "details": { "title": ["…"] }, "request_id": "01J…" } }request_id is a ULID on every response — in the body on errors,
and in the X-Request-Id header on successes too — logged server-side:
email it to support and the call is traceable without guesswork.
details appears only when there is something field-specific to report.
| Type | Status | Meaning |
|---|---|---|
unauthenticated | 401 | Missing, revoked, or expired token. |
forbidden | 403 | Wrong ability, or the plan grants no API access. |
not_found | 404 | No such record for this account. |
malformed_json | 400 | The body is not valid JSON. |
payload_too_large | 413 | The body exceeds the 256 KB limit. |
unsupported_media_type | 415 | Writes must use Content-Type: application/json. |
validation_failed | 422 | Unknown fields, bad values, unknown filters. |
invalid_cursor | 422 | Tampered, foreign, or filter-mismatched cursor — start over from page one. |
idempotency_in_progress | 409 | Same key still executing — wait a second and retry with the same key. |
idempotency_key_reused | 422 | Same key, different request — a fresh operation needs a fresh key. |
rate_limited | 429 | Budget exhausted — honor Retry-After. |
service_unavailable | 503 | API disabled or limiter store down — retry with backoff. |
Timestamps are ISO 8601 UTC (Z); input without an offset is a 422. Ids
are integer primary keys. No response ever contains user_id, token
hashes, OAuth tokens, or raw provider metadata.
Pagination, filtering, sorting
Cursor-based: ?cursor= + ?per_page= (default 25, cap 100). Cursors are
signed and bound to the resource, user, parent, and filters — a cursor
from another filter set is a 422 invalid_cursor, never a silent restart.
Lists never count; meta is {per_page, next_cursor, has_more}.
Filters are whitelisted per endpoint (see openapi.json); unknown keys
are a 422. There is no sorting API in v1.0 — lists are id DESC (smart
feeds page by snapshot position).
Idempotency
All POST/PATCH require Idempotency-Key; DELETE accepts it. 1–255
printable ASCII, scoped to the token owner, kept 24 h.
- Replaying a key returns the original status and body without
executing again — safe to retry on any network doubt. Only
Content-TypeandLocationare restored;X-Request-IdandCache-Controlare fresh on every replay. - The same key with different method/path/query/body is a 422
idempotency_key_reused. - A key still executing elsewhere is a 409 with
Retry-After: 1. - After expiry a key is a new logical operation.
- Rotating to a token with sufficient abilities can replay a key; a lesser token gets 403 on replay.
Webhooks
Subscribe in Settings → API (up to 20 endpoints per user, public
HTTPS only). Events: post.published, publisher.run.failed,
channel.token_expired. Payloads are versioned envelopes
(WebhookPostPublished, WebhookPublisherRunFailed,
WebhookChannelTokenExpired in openapi.json); a breaking shape change
ships a new version, never a silent field change.
Delivery guarantees:
- At-least-once, unordered. Dedupe on the stable
event_id— a UUID forpost.published,publisher.run.failed:<uuid>andchannel.token_expired:<integrationId>:<uuid>for the other two.X-Feedigy-Deliverycarries the per-endpoint delivery row id (an integer, stable across retries of that delivery) with the same body. - Five attempts (initial send plus retries after ~10 s, ~60 s,
~5 min, ~30 min, plus jitter). A bounded
Retry-Afteron your 429/503 wins over the ladder. No redirects, 10 s HTTP timeout. - Five terminally failed deliveries in a row auto-disable the endpoint with one in-app notice; any success — or re-enabling — resets the counter.
- Signing:
X-Feedigy-Signature: t=<ts>,v1=<hmac>over<ts>.<exact raw body>, HMAC-SHA256 with your secret. Fresh timestamp per attempt, stable body and delivery id. Reject timestamps outside a 5-minute window, compare in constant time. During the 24 h after a secret rotation both signatures are sent — accept either. - Payloads are thin (ids plus summaries, under 64 KB) and mirror the public v1 resources: no tokens, no provider payloads, no secrets.
- The delivery log in Settings shows status codes and redacted excerpts, filters failures, and redelivers on demand. Disabling an endpoint parks its pending rows; re-enabling sends them.
Versioning
/v1/… in the path. A v2 ships alongside v1; v1 is not removed without
a published deprecation window and Sunset headers. Additive changes
(new fields, new endpoints, new events) do not bump the version;
breaking shape changes ship a new payload version (webhooks) or a new
path version (REST) — removal or rename requires v2.
Keep posting through busy weeks
Set sources, keywords and posting hours once. Feedigy posts to your channels while you work on other things.
No card required. The Free plan has no time limit.
