Skip to content

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:write
posts:read posts:write
channels:read publishers:read
account:read

A 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.

MembershipRequests/min (per user)
free (the fallback new users get)30
starter60
pro180
agency600
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.

TypeStatusMeaning
unauthenticated401Missing, revoked, or expired token.
forbidden403Wrong ability, or the plan grants no API access.
not_found404No such record for this account.
malformed_json400The body is not valid JSON.
payload_too_large413The body exceeds the 256 KB limit.
unsupported_media_type415Writes must use Content-Type: application/json.
validation_failed422Unknown fields, bad values, unknown filters.
invalid_cursor422Tampered, foreign, or filter-mismatched cursor — start over from page one.
idempotency_in_progress409Same key still executing — wait a second and retry with the same key.
idempotency_key_reused422Same key, different request — a fresh operation needs a fresh key.
rate_limited429Budget exhausted — honor Retry-After.
service_unavailable503API 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-Type and Location are restored; X-Request-Id and Cache-Control are 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 for post.published, publisher.run.failed:<uuid> and channel.token_expired:<integrationId>:<uuid> for the other two. X-Feedigy-Delivery carries 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-After on 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.