# Headless selection

Build your own picker for Facebook Pages, Instagram accounts, LinkedIn pages, Pinterest boards, business locations and more, after the user approves on the platform.

Some platforms grant access to a person, not to the thing you want to post as. After a Facebook login you still need to know which Page; after a Google login, which business location. By default Pozy shows a picker for this step. In headless mode you build the picker in your own app and Pozy only handles the OAuth around it.

Use headless mode when the connect has to look native inside your product, when you want to filter the choices (for example, only locations in one country), or when you connect accounts on behalf of users who never see a Pozy page.

## Platforms with a selection step

| Platform | `step` on the callback | The user chooses | List with | Connect with |
| --- | --- | --- | --- | --- |
| Facebook | `select_page` | One or more Pages | [`GET /v1/connect/facebook/select-page`](https://pozy.ai/docs/api/listFacebookPages) | [`POST /v1/connect/facebook/select-page`](https://pozy.ai/docs/api/selectFacebookPage) |
| Instagram (with `loginMethod=facebook_login`) | `select_account` | Pages with a linked Instagram professional account | [`GET /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/listInstagramPages) | [`POST /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/selectInstagramAccount) |
| LinkedIn | `select_organization` | Their personal profile or company pages they administer | [`GET /v1/connect/pending-data`](https://pozy.ai/docs/api/getPendingOAuthData), then [`GET /v1/connect/linkedin/organizations`](https://pozy.ai/docs/api/listLinkedInOrganizations) for logos | [`POST /v1/connect/linkedin/select-organization`](https://pozy.ai/docs/api/selectLinkedInOrganization) |
| Pinterest | `select_board` | A default board | [`GET /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/listPinterestBoardsForSelection) | [`POST /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/selectPinterestBoard) |
| Google Business Profile | `select_location` | One or more locations | [`GET /v1/connect/googlebusiness/locations`](https://pozy.ai/docs/api/listGoogleBusinessLocations) | [`POST /v1/connect/googlebusiness/select-location`](https://pozy.ai/docs/api/selectGoogleBusinessLocation) |
| Snapchat | `select_public_profile` | A Public Profile | [`GET /v1/connect/snapchat/select-profile`](https://pozy.ai/docs/api/listSnapchatProfiles) | [`POST /v1/connect/snapchat/select-profile`](https://pozy.ai/docs/api/selectSnapchatProfile) |
| WhatsApp | `select_phone_number` | A number in a WhatsApp Business Account | [`GET /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/listWhatsAppPhoneNumbers) | [`POST /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/completeWhatsAppPhoneSelection) |
| Slack | `select_channel` | One or more channels | [`GET /v1/connect/slack`](https://pozy.ai/docs/api/listSlackChannels) with `pendingDataToken` | [`POST /v1/connect/slack`](https://pozy.ai/docs/api/connectSlackChannel) |
| Google Analytics | `select_property` | A GA4 property | [`GET /v1/connect/googleanalytics/properties`](https://pozy.ai/docs/api/listGoogleAnalyticsProperties) | [`POST /v1/connect/googleanalytics/select-property`](https://pozy.ai/docs/api/selectGoogleAnalyticsProperty) |

The default Instagram login (`instagram_login`) has no selection step.

## How the flow works

### Start with `headless=true`

Add `headless=true` and a `redirect_url` to the connect call. Headless mode needs a `redirect_url`: without one, Pozy falls back to its own picker. It cannot be combined with `welcome=true` or `handoff=true`.

```bash
curl "https://api.pozy.ai/v1/connect/facebook?profileId=6650c1f2a9e4b7d3c8a1f0e2&headless=true&redirect_url=https://app.example.com/connect/facebook" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

Send the browser to the returned `authUrl`.

### Read the callback

After consent, the browser lands on your `redirect_url` with the data your picker needs instead of a finished account:

```text
https://app.example.com/connect/facebook?profileId=6650c1f2a9e4b7d3c8a1f0e2&platform=facebook&step=select_page&tempToken=...&userProfile=...&connect_token=...
```

| Parameter | What it is |
| --- | --- |
| `step` | Which selection to show. See the table above. |
| `tempToken` | A short-lived token for this one flow. Pass it to the list and connect calls. |
| `userProfile` | JSON describing the person who logged in. Pass it back unchanged on the connect call. |
| `pendingDataToken` | Sent instead of the full lists when they are too long for a URL (LinkedIn, Google Business Profile, Slack, Google Analytics, and some Pinterest and Snapchat flows). |
| `connect_token` | Send it as the `X-Connect-Token` header on the list and connect calls. Pinterest and Snapchat require it. |

`userProfile` arrives URL-encoded JSON. Depending on your framework it may still be encoded once after the query string is parsed, so parse defensively:

```ts
const raw = url.searchParams.get("userProfile") ?? "{}";
const userProfile = JSON.parse(raw.trim().startsWith("{") ? raw : decodeURIComponent(raw));
```

### List the options

From your server, call the list endpoint for that `step` with your API key:

```bash cURL
curl "https://api.pozy.ai/v1/connect/facebook/select-page?profileId=6650c1f2a9e4b7d3c8a1f0e2&tempToken=$TEMP_TOKEN" \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "X-Connect-Token: $CONNECT_TOKEN"
```
```ts Node
const res = await fetch(`https://api.pozy.ai/v1/connect/facebook/select-page?${new URLSearchParams({ profileId: "6650c1f2a9e4b7d3c8a1f0e2", tempToken })}`, {
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "X-Connect-Token": connectToken,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// data.pages: [{ id, name, username, category }]
```
```python Python
resp = requests.get(
    "https://api.pozy.ai/v1/connect/facebook/select-page",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "X-Connect-Token": connect_token,
    },
    params={"profileId": "6650c1f2a9e4b7d3c8a1f0e2", "tempToken": temp_token},
    timeout=30,
)
pages = resp.json()["pages"]
```

```json
{
  "pages": [
    { "id": "104857221093311", "name": "Acme Coffee", "username": "acmecoffee", "category": "Coffee shop" },
    { "id": "104857221093312", "name": "Acme Coffee Madrid", "username": "acmemadrid", "category": "Cafe" }
  ],
  "truncated": false
}
```

### Show your picker

Render the options however you like. Keep the `tempToken`, `userProfile` and `connect_token` in your server-side session, not in hidden form fields.

### Connect the choice

Post the choice back. The response contains the new account, and a `redirect_url` (your own, with `connected`, `accountId` and the other result parameters added) you can send the browser to.

```bash cURL
curl -X POST "https://api.pozy.ai/v1/connect/facebook/select-page" \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "X-Connect-Token: $CONNECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1f2a9e4b7d3c8a1f0e2",
    "pageIds": ["104857221093311", "104857221093312"],
    "tempToken": "'"$TEMP_TOKEN"'",
    "userProfile": { "id": "10228841765", "name": "Ana Ruiz" },
    "redirect_url": "https://app.example.com/connected"
  }'
```
```ts Node
const resultRes = await fetch("https://api.pozy.ai/v1/connect/facebook/select-page", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "X-Connect-Token": connectToken,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6650c1f2a9e4b7d3c8a1f0e2",
    pageIds: ["104857221093311", "104857221093312"],
    tempToken,
    userProfile,
    redirect_url: "https://app.example.com/connected",
  }),
});
const result = await resultRes.json();
if (!resultRes.ok) throw new Error(result.error);
// result.accounts: one entry per connected Page; result.failed: Pages that could not connect
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/connect/facebook/select-page",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "X-Connect-Token": connect_token,
    },
    json={
        "profileId": "6650c1f2a9e4b7d3c8a1f0e2",
        "pageIds": ["104857221093311", "104857221093312"],
        "tempToken": temp_token,
        "userProfile": user_profile,
        "redirect_url": "https://app.example.com/connected",
    },
    timeout=30,
)
result = resp.json()
```

```json
{
  "message": "Facebook page connected successfully",
  "redirect_url": "https://app.example.com/connected?connected=facebook&profileId=6650c1f2a9e4b7d3c8a1f0e2&accountId=6651d3a7b2c9e4f1a0b8c7d6&stage=select_page",
  "account": {
    "accountId": "6651d3a7b2c9e4f1a0b8c7d6",
    "platform": "facebook",
    "username": "acmecoffee",
    "isActive": true,
    "selectedPageName": "Acme Coffee"
  },
  "accounts": [
    { "accountId": "6651d3a7b2c9e4f1a0b8c7d6", "platform": "facebook", "username": "acmecoffee" },
    { "accountId": "6651d3a7b2c9e4f1a0b8c7d7", "platform": "facebook", "username": "acmemadrid" }
  ],
  "failed": []
}
```

## Several choices at once

Facebook Pages, Instagram accounts, LinkedIn pages, business locations and Slack channels can be connected in one call. Each choice becomes its own account.

| Platform | Single | Several |
| --- | --- | --- |
| Facebook, Instagram | `pageId` | `pageIds` |
| LinkedIn | `accountType` + `selectedOrganization` | `selections`: a list of `{ accountType, selectedOrganization }` |
| Google Business Profile | `locationId` | `locations`: a list of `{ locationId }` |
| Slack | `channelId` | `channelIds` |

The response lists what connected in `accounts` and what did not in `failed`, each with a `code` and `message`. One failed Page does not stop the others.

## LinkedIn: personal profile or company page

LinkedIn returns a `pendingDataToken`. Read the organizations with `GET /v1/connect/pending-data?token=...`, then fetch logos and vanity names for them with `GET /v1/connect/linkedin/organizations?tempToken=...&orgIds=...`. Offer the user's personal profile as the first option.

```json
{
  "profileId": "6650c1f2a9e4b7d3c8a1f0e2",
  "tempToken": "...",
  "userProfile": { "id": "Xk3pq9", "name": "Ana Ruiz" },
  "selections": [
    { "accountType": "personal" },
    {
      "accountType": "organization",
      "selectedOrganization": { "id": "90412233", "urn": "urn:li:organization:90412233", "name": "Acme Coffee" }
    }
  ],
  "redirect_url": "https://app.example.com/connected"
}
```

## Timing

| Token | Lifetime |
| --- | --- |
| The whole connect flow | 30 minutes from the connect call |
| `pendingDataToken` | Readable for 1 hour, as often as you like, until the selection completes |

After the selection completes, the tokens stop working. If the user abandons your picker, start a new connect.

## Change a selection later

You do not need a new OAuth to switch the Page, organization, board or location behind an existing account:

| Platform | Endpoint |
| --- | --- |
| Facebook | [`PUT /v1/accounts/{accountId}/facebook-page`](https://pozy.ai/docs/api/updateFacebookPage) with `selectedPageId` |
| LinkedIn | [`PUT /v1/accounts/{accountId}/linkedin-organization`](https://pozy.ai/docs/api/updateLinkedInOrganization) |
| Pinterest | [`PUT /v1/accounts/{accountId}/pinterest-boards`](https://pozy.ai/docs/api/updatePinterestBoards) |
| Google Business Profile | [`PUT /v1/accounts/{accountId}/gmb-locations`](https://pozy.ai/docs/api/updateGmbLocation) |

Each has a matching `GET` that lists the options the account can switch to.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/pending-data`](https://pozy.ai/docs/api/getPendingOAuthData) | Get pending OAuth data |
| [`GET /v1/connect/facebook/select-page`](https://pozy.ai/docs/api/listFacebookPages) | List Facebook pages |
| [`POST /v1/connect/facebook/select-page`](https://pozy.ai/docs/api/selectFacebookPage) | Select Facebook page |
| [`GET /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/listInstagramPages) | List Pages with a linked Instagram account |
| [`POST /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/selectInstagramAccount) | Select the Page whose Instagram account to connect |
| [`GET /v1/connect/linkedin/organizations`](https://pozy.ai/docs/api/listLinkedInOrganizations) | List LinkedIn orgs |
| [`POST /v1/connect/linkedin/select-organization`](https://pozy.ai/docs/api/selectLinkedInOrganization) | Select LinkedIn org |
| [`GET /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/listPinterestBoardsForSelection) | List Pinterest boards |
| [`POST /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/selectPinterestBoard) | Select Pinterest board |
| [`GET /v1/connect/googlebusiness/locations`](https://pozy.ai/docs/api/listGoogleBusinessLocations) | List Google Business Profile locations |
| [`POST /v1/connect/googlebusiness/select-location`](https://pozy.ai/docs/api/selectGoogleBusinessLocation) | Select Google Business Profile location |
| [`GET /v1/connect/snapchat/select-profile`](https://pozy.ai/docs/api/listSnapchatProfiles) | List Snapchat profiles |
| [`POST /v1/connect/snapchat/select-profile`](https://pozy.ai/docs/api/selectSnapchatProfile) | Select Snapchat profile |
| [`GET /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/listWhatsAppPhoneNumbers) | List numbers for selection |
| [`POST /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/completeWhatsAppPhoneSelection) | Complete number selection |
| [`GET /v1/connect/slack`](https://pozy.ai/docs/api/listSlackChannels) | List Slack channels for the channel picker |
| [`POST /v1/connect/slack`](https://pozy.ai/docs/api/connectSlackChannel) | Connect a Slack channel |
| [`GET /v1/connect/googleanalytics/properties`](https://pozy.ai/docs/api/listGoogleAnalyticsProperties) | List Google Analytics properties for a pending connect |
| [`POST /v1/connect/googleanalytics/select-property`](https://pozy.ai/docs/api/selectGoogleAnalyticsProperty) | Connect the chosen Google Analytics property |
