# Keep your app in sync (/docs/recipes/sync-posts)



If your app shows posts and their status, keep a copy in your database. Webhooks tell you about changes as they happen; a regular catch-up call fills any gap, for example while your server was down.

1. Subscribe to post events [#1-subscribe-to-post-events]

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl -X POST https://feedhorizon.com/api/v1/webhooks \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Sync",
        "url": "https://api.example.com/webhooks/feedhorizon",
        "events": ["post.scheduled", "post.updated", "post.deleted", "post.target.published", "post.target.failed", "post.published", "post.partial", "post.failed"]
      }'
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Handle each event idempotently: the same event can arrive twice (it carries the same `id` then), and events can arrive out of order. The simplest robust handler loads the post fresh instead of applying the event:

<CodeBlockTabs defaultValue="Node.js">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Node.js">
      Node.js
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Node.js">
    ```js
    async function onEvent({ event, data }) {
        if (event === 'post.deleted') return db.posts.delete(data.postId);
        const res = await fetch(`https://feedhorizon.com/api/v1/posts/${data.postId}`, {
            headers: { Authorization: `Bearer ${process.env.FEEDHORIZON_API_KEY}` },
        });
        if (res.status === 404) return db.posts.delete(data.postId);
        const { data: post } = await res.json();
        await db.posts.upsert(post.id, post);
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    def on_event(body):
        data = body["data"]
        if body["event"] == "post.deleted":
            return db.posts.delete(data["postId"])
        res = requests.get(f"https://feedhorizon.com/api/v1/posts/{data['postId']}", headers=headers)
        if res.status_code == 404:
            return db.posts.delete(data["postId"])
        post = res.json()["data"]
        db.posts.upsert(post["id"], post)
    ```
  </CodeBlockTab>
</CodeBlockTabs>

2. Catch up regularly [#2-catch-up-regularly]

Every few minutes, ask for the posts that changed since your last sync. Store the time before each call and pass it next time:

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Node.js">
      Node.js
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl "https://feedhorizon.com/api/v1/posts?updatedSince=2026-11-10T08:00:00Z" \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY"
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    const startedAt = new Date().toISOString();
    let cursor = null;
    do {
        const url = new URL('https://feedhorizon.com/api/v1/posts');
        url.searchParams.set('updatedSince', lastSync);
        if (cursor) url.searchParams.set('cursor', cursor);
        const page = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FEEDHORIZON_API_KEY}` } }).then((r) => r.json());
        for (const post of page.data) await db.posts.upsert(post.id, post);
        cursor = page.nextCursor;
    } while (cursor);
    lastSync = startedAt;
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    started_at = datetime.now(timezone.utc).isoformat()
    cursor = None
    while True:
        params = {"updatedSince": last_sync, **({"cursor": cursor} if cursor else {})}
        page = requests.get("https://feedhorizon.com/api/v1/posts", headers=headers, params=params).json()
        for post in page["data"]:
            db.posts.upsert(post["id"], post)
        cursor = page["nextCursor"]
        if not cursor:
            break
    last_sync = started_at
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Deleted posts do not show up in this list; only the `post.deleted` webhook tells you about them.

3. A calendar view [#3-a-calendar-view]

For a calendar, `GET /posts?from=…&to=…` returns the posts whose publish time (published, else scheduled) falls in that range, oldest first. Filter by `profileId` and `status` as needed. See [List posts](/docs/api/posts/list).
