# Onboard a customer (/docs/recipes/onboard-a-customer)



This recipe is for agencies and products that post for their customers. It creates a profile per customer, sends a connect link, and waits for the account to arrive. The background is in [Agencies and SaaS](/docs/concepts/multi-tenant).

You need a key with access to all profiles (creating profiles needs it) and the scopes `profiles:write` and `platform_invites:write`.

1. Create the profile [#1-create-the-profile]

Store your customer id in `externalId`, so you can find the profile without keeping our id.

<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/profiles \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Soup Club", "timezone": "Europe/Berlin", "externalId": "cus_8812" }'
    ```
  </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 profile = await fetch(`${API}/profiles`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ name: 'Soup Club', timezone: 'Europe/Berlin', externalId: customer.id }),
    }).then((r) => r.json());
    ```
  </CodeBlockTab>

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

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

    profile = requests.post(
        f"{API}/profiles",
        headers=headers,
        json={"name": "Soup Club", "timezone": "Europe/Berlin", "externalId": customer_id},
    ).json()
    ```
  </CodeBlockTab>
</CodeBlockTabs>

A second profile with the same `externalId` answers `409 external_id_taken`. To make the step safe to repeat, look first: `GET /profiles?externalId=cus_8812`.

2. Send a connect link [#2-send-a-connect-link]

Create one link per platform the customer wants to connect.

<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/platform-invites \
      -H "Authorization: Bearer $FEEDHORIZON_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "profileId": "prof_…", "platform": "linkedin" }'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    const invite = await fetch(`${API}/platform-invites`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ profileId: profile.data.id, platform: 'linkedin' }),
    }).then((r) => r.json());
    sendToCustomer(invite.data.inviteUrl); // shown only in this response
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    invite = requests.post(
        f"{API}/platform-invites",
        headers=headers,
        json={"profileId": profile["data"]["id"], "platform": "linkedin"},
    ).json()
    send_to_customer(invite["data"]["inviteUrl"])  # shown only in this response
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The link works once and for 7 days. The customer signs in at the platform; they need no FeedHorizon account. [Connect links](/docs/guides/connect-links) shows what they see.

3. Wait for the account [#3-wait-for-the-account]

When the customer is done, FeedHorizon sends `account.connected`:

```json
{
  "id": "evt_…",
  "event": "account.connected",
  "timestamp": "2026-11-10T08:00:00.000Z",
  "data": {
    "accountId": "acc_…",
    "profileId": "prof_…",
    "platform": "linkedin",
    "displayName": "Soup Club"
  }
}
```

Find the customer by `profileId`, store the `accountId`, and show the account as connected in your app. Without a webhook, poll `GET /accounts?profileId=…` or check `usedAt` in `GET /platform-invites?profileId=…`.

4. Post for the customer [#4-post-for-the-customer]

Create posts in the customer's profile as usual, with `platforms` or the stored `accountIds`. See [Post an image to several platforms](/docs/recipes/post-with-image).

Keep connections healthy [#keep-connections-healthy]

Some platforms end connections after a while, or the customer revokes access. Listen for:

* `account.expiring`: the connection ends in 7 days and cannot be renewed automatically. Send the customer a new connect link.
* `account.needs_reauth`: the connection has stopped working. Posts to it fail with `account_needs_reauth` until the customer connects again, through a new connect link.

Reconnecting the same social account to the same profile keeps its account id, so the ids you stored stay valid.
