# Post an image to several platforms (/docs/recipes/post-with-image)



This recipe posts one image with a text to LinkedIn and Instagram, with a shorter text for X. It uses four calls: import the image, check the post, create it, then wait for the webhook.

You need an API key with `posts:write` and the id of a profile that has these accounts connected (`GET /profiles`, `GET /accounts?profileId=…`).

1. Import the image [#1-import-the-image]

<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/media \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://example.com/launch.jpg" }'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    const API = 'https://feedhorizon.com/api/v1';
    const headers = {
        Authorization: `Bearer ${process.env.FEEDHORIZON_API_KEY}`,
        'Content-Type': 'application/json',
    };

    const media = await fetch(`${API}/media`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ url: 'https://example.com/launch.jpg' }),
    }).then((r) => r.json());
    const mediaId = media.data.id; // med_…
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os, requests

    API = "https://feedhorizon.com/api/v1"
    headers = {"Authorization": f"Bearer {os.environ['FEEDHORIZON_API_KEY']}"}

    media = requests.post(f"{API}/media", headers=headers, json={"url": "https://example.com/launch.jpg"}).json()
    media_id = media["data"]["id"]  # med_…
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The call answers once the file is stored. Files on your own machine go through a [direct upload](/docs/guides/media-uploads#upload-directly) instead.

2. Check the post [#2-check-the-post]

`POST /posts/validate` runs every check without creating anything: text length per platform, media types and counts, connected accounts, the time.

```json title="post.json"
{
  "profileId": "prof_…",
  "content": "Our new soup subscription starts today: five recipes a week, delivered as a shopping list every Sunday.",
  "mediaIds": ["med_…"],
  "targets": [
    { "accountId": "acc_LINKEDIN…" },
    { "accountId": "acc_INSTAGRAM…" },
    { "accountId": "acc_X…", "content": "Soup subscription is live: 5 recipes a week 🍲" }
  ],
  "scheduledFor": "2026-11-12T09:00:00+01:00"
}
```

<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/validate \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -d @post.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    // post: the body shown in post.json above
    const check = await fetch(`${API}/posts/validate`, { method: 'POST', headers, body: JSON.stringify(post) }).then((r) => r.json());
    if (!check.data.valid) {
        for (const e of check.data.errors) console.log(e.code, e.param, e.message);
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    # post: the body shown in post.json above
    check = requests.post(f"{API}/posts/validate", headers=headers, json=post).json()
    if not check["data"]["valid"]:
        for e in check["data"]["errors"]:
            print(e["code"], e.get("param"), e["message"])
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Fix what `errors` lists. `warnings` do not block the post; `account_not_active` means an account must be reconnected before the post goes out. This step is optional: `POST /posts` runs the same checks and answers `400` with every problem in `error.details`.

3. Create the post [#3-create-the-post]

Send the same body to `POST /posts`, with an `Idempotency-Key` so a retry after a timeout does not post twice.

<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 \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: soup-launch-2026-11-12" \
      -d @post.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    const created = await fetch(`${API}/posts`, {
        method: 'POST',
        headers: { ...headers, 'Idempotency-Key': 'soup-launch-2026-11-12' },
        body: JSON.stringify(post),
    }).then((r) => r.json());
    console.log(created.data.id, created.data.status); // post_… scheduled
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    created = requests.post(
        f"{API}/posts",
        headers={**headers, "Idempotency-Key": "soup-launch-2026-11-12"},
        json=post,
    ).json()
    print(created["data"]["id"], created["data"]["status"])  # post_… scheduled
    ```
  </CodeBlockTab>
</CodeBlockTabs>

4. Learn when it is live [#4-learn-when-it-is-live]

Subscribe a [webhook](/docs/guides/webhooks) to `post.target.published` and `post.target.failed` to hear about each account as soon as it is done, or to `post.published`, `post.partial` and `post.failed` for the whole post. `targets[].platformUrl` links to the live post.

Without a webhook, `GET /posts/{id}` shows the same: the post's `status` and each target's `status`, `platformUrl` and `error`.

Instagram and other platforms that need media [#instagram-and-other-platforms-that-need-media]

Instagram and Pinterest need at least one image or video, TikTok a video. A post with these accounts and no media fails the check with `media_required`. [Platform support](/docs/platforms/overview) lists the rules of every platform.
