# Handle failed posts (/docs/recipes/handle-failed-posts)



A post can fail on one account and succeed on the others. This recipe listens for failures per account, decides from the error who has to act, and retries once the cause is gone.

1. Listen for failures [#1-listen-for-failures]

Create a webhook for the events that matter here:

<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": "Failures",
        "url": "https://api.example.com/webhooks/feedhorizon",
        "events": ["post.target.failed", "account.needs_reauth", "account.expiring"]
      }'
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`post.target.failed` arrives once per account that failed for good: after FeedHorizon's own retries, or at once for errors that will not go away by trying again.

2. Decide who acts [#2-decide-who-acts]

Every failure carries an `error` with a `code`, a `category` and a `source`. `source` tells you who can fix it:

| `source`   | Who acts                                                          | Typical codes                                                       |
| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| `user`     | You or your customer                                              | `account_needs_reauth`, `platform_auth_failed`, `platform_rejected` |
| `platform` | Nobody; the platform was busy or down and FeedHorizon tried again | `platform_rate_limited`, `platform_unavailable`                     |
| `system`   | Check, then retry or contact support                              | `timeout`, `publish_failed`                                         |

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

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

  <CodeBlockTab value="Node.js">
    ```js
    app.post('/webhooks/feedhorizon', express.raw({ type: 'application/json' }), async (req, res) => {
        if (!verifyFeedHorizonSignature(req.body.toString(), req.get('X-Webhook-Signature'), process.env.FEEDHORIZON_WEBHOOK_SECRET)) {
            return res.sendStatus(401);
        }
        res.sendStatus(200); // answer fast, work afterwards

        const { event, data } = JSON.parse(req.body);
        if (event !== 'post.target.failed') return;

        switch (data.error.category) {
            case 'auth_expired':
                // The account must be connected again: send the customer a connect link.
                return askToReconnect(data.profileId, data.accountId, data.platform);
            case 'rejected':
                // The platform refused this content: show the message to whoever wrote it.
                return notifyAuthor(data.postId, data.platform, data.error.message);
            default:
                // Rate limits, outages, timeouts: log it and alert if it keeps happening.
                return logFailure(data);
        }
    });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    @app.post("/webhooks/feedhorizon")
    def feedhorizon_webhook():
        raw = request.get_data(as_text=True)
        if not verify_signature(raw, request.headers.get("X-Webhook-Signature"), os.environ["FEEDHORIZON_WEBHOOK_SECRET"]):
            abort(401)

        body = json.loads(raw)
        if body["event"] == "post.target.failed":
            data = body["data"]
            category = data["error"]["category"]
            if category == "auth_expired":
                ask_to_reconnect(data["profileId"], data["accountId"], data["platform"])
            elif category == "rejected":
                notify_author(data["postId"], data["platform"], data["error"]["message"])
            else:
                log_failure(data)
        return "", 200
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`verifyFeedHorizonSignature` and `verify_signature` are on the [Webhooks](/docs/guides/webhooks#verify-signatures) page. Every code with what to do is in [Publishing errors](/docs/api/errors#publishing-errors).

3. Retry [#3-retry]

Once the cause is fixed (the account is connected again, the platform is back), send the failed accounts again:

<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 -X POST https://feedhorizon.com/api/v1/posts/post_…/retry \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY"
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    await fetch(`https://feedhorizon.com/api/v1/posts/${postId}/retry`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${process.env.FEEDHORIZON_API_KEY}` },
    });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    requests.post(f"https://feedhorizon.com/api/v1/posts/{post_id}/retry", headers=headers)
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Only the failed accounts are sent; accounts that already published stay as they are.

* After **`account_needs_reauth`** or **`platform_auth_failed`**, wait for `account.connected` for that account, then retry the posts that failed on it.
* After **`platform_rejected`**, retrying the same text fails the same way. Change the post first. If other accounts already published, the post can no longer be edited: [duplicate it](/docs/concepts/post-lifecycle#duplicate-to-change-a-live-post), keep only the failed account and fix the text.
* After **`timeout`**, look at the account on the platform first. The post may have gone live even though the platform did not confirm it in time; a retry would post it twice.

4. Prevent the next failure [#4-prevent-the-next-failure]

* Run `POST /posts/validate` before creating posts. It catches text that is too long, media a platform does not take, and accounts that need a reconnect (warning `account_not_active`).
* React to `account.expiring`: the connection ends in 7 days. A new connect link before then avoids failures.
* Spread many posts to the same account over time; platforms limit how often one account may post.
