# Build a platform

Give each of your customers their own profile, connect their accounts, act on their behalf and route every webhook back to the right customer.

If you build a product on Pozy, such as a scheduler, an agency tool or a CRM with social features, your customers' accounts all live in your one workspace. Profiles keep them apart. This page is the blueprint: one profile per customer, your own id on it, and events that come back already labelled with that id.

## The model

| Your side | Pozy side |
| --- | --- |
| Customer `cus_8f3a2` | Profile with `externalId: "cus_8f3a2"` |
| Their Instagram, TikTok, LinkedIn | Connected accounts inside that profile |
| Your backend | One full-access API key, or one scoped key per customer |
| Your webhook handler | One endpoint for everyone, or one per customer filtered by profile |

Requests carry the `profileId` in. Webhooks carry `profiles[].externalId` out, so routing an event needs no lookup.

A customer with three Facebook Pages and two Instagram accounts connects all five into their single profile. Do not create one profile per account.

## Step 1: create a profile when a customer signs up

Use your own customer id as the `externalId` and keep the returned `_id` on your customer record.

```bash cURL
curl -X POST https://api.pozy.ai/v1/profiles \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Corp", "externalId": "cus_8f3a2" }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/profiles", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Acme Corp", externalId: "cus_8f3a2" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
await db.customers.update("cus_8f3a2", { pozyProfileId: data.profile._id });
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/profiles",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"name": "Acme Corp", "externalId": "cus_8f3a2"},
)
profile_id = resp.json()["profile"]["_id"]
```

`externalId` is unique in your workspace. If a signup is retried, the second create returns `409` with `code: "profile_external_id_conflict"` and the existing id in `details.existingProfileId`, so you never end up with two profiles for one customer. Lost the id? `GET /v1/profiles?externalId=cus_8f3a2` finds it ([profiles](https://pozy.ai/docs/build/profiles)).

## Step 2: let the customer connect their accounts

Ask for a connect URL with the customer's `profileId` and your own `redirect_url`, then send the customer's browser there.

```bash cURL
curl "https://api.pozy.ai/v1/connect/instagram?profileId=6720f3b1c9e4a85d2f0b7c14&redirect_url=https://app.example.com/integrations/done" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const query = new URLSearchParams({ profileId, redirect_url: "https://app.example.com/integrations/done" });
const connectRes = await fetch(`https://api.pozy.ai/v1/connect/instagram?${query}`, {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await connectRes.json();
if (!connectRes.ok) throw new Error(data.error);
res.redirect(data.authUrl);
```
```python Python
resp = requests.get(
    "https://api.pozy.ai/v1/connect/instagram",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"profileId": profile_id, "redirect_url": "https://app.example.com/integrations/done"},
)
auth_url = resp.json()["authUrl"]  # redirect the customer here
```

The customer signs in on the platform, picks the Page or channel if the platform asks, and lands back on your `redirect_url`. Every screen in between is hosted by Pozy. You can put your own brand on them with the [welcome screen](https://pozy.ai/docs/connect/welcome-screen), or draw the selection steps in your own UI with [headless connect](https://pozy.ai/docs/connect/headless).

On your server, listen for `account.connected`. It names the customer through `externalId`:

```json
{
  "id": "evt_2c7e1a9f04b8d3e5",
  "event": "account.connected",
  "timestamp": "2026-10-30T14:02:11.000Z",
  "profiles": [{ "id": "6720f3b1c9e4a85d2f0b7c14", "externalId": "cus_8f3a2" }],
  "account": {
    "accountId": "6720f41a8d3be96c1e5a02f7",
    "profileId": "6720f3b1c9e4a85d2f0b7c14",
    "platform": "instagram",
    "username": "acmecorp"
  }
}
```

`account.disconnected` arrives the same way when access ends, so you can show the customer a "Reconnect" button.

## Step 3: act on their behalf

Every call that works on accounts, posts, conversations or analytics accepts the customer's `profileId` or their `accountId`s. To post to everything a customer connected:

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cus_8f3a2-post-1042" \
  -d '{
    "content": "Spring menu is live.",
    "mediaItems": [{ "type": "image", "url": "https://pozy.ai/media/spring-menu.jpg" }],
    "platforms": [
      { "platform": "instagram", "accountId": "6720f41a8d3be96c1e5a02f7" },
      { "platform": "facebook", "accountId": "6720f52b9a1cd47e3f8b06a2" }
    ],
    "scheduledFor": "2026-11-04T11:30:00",
    "timezone": "America/Chicago"
  }'
```
```ts Node
const listedRes = await fetch(`https://api.pozy.ai/v1/accounts?${new URLSearchParams({ profileId })}`, {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const listed = await listedRes.json();
if (!listedRes.ok) throw new Error(listed.error);
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": `cus_8f3a2-post-${draft.id}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: draft.text,
    mediaItems: draft.media,
    platforms: listed.accounts.map((a) => ({ platform: a.platform, accountId: a._id })),
    scheduledFor: draft.when,
    timezone: "America/Chicago",
  }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
accounts = requests.get(
    "https://api.pozy.ai/v1/accounts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"profileId": profile_id},
).json()["accounts"]
requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": f"cus_8f3a2-post-{draft_id}",
    },
    json={
        "content": "Spring menu is live.",
        "platforms": [{"platform": a["platform"], "accountId": a["_id"]} for a in accounts],
        "scheduledFor": "2026-11-04T11:30:00",
        "timezone": "America/Chicago",
    },
)
```

Derive idempotency keys from your own ids (`cus_8f3a2-post-1042`) so a retried job can never publish twice ([idempotency](https://pozy.ai/docs/getting-started/idempotency)).

## Step 4: route webhooks back to the customer

Register one endpoint for all customers and route on `profiles[0].externalId`, or give large customers their own endpoint filtered with `profileIds`. [Route webhooks to tenants](https://pozy.ai/docs/build/route-webhooks) has a complete router.

## Keys: one master key or one per customer

A single full-access key on your backend is enough for most platforms. Add [per-customer keys](https://pozy.ai/docs/build/tenant-keys) when part of your system should only ever see one customer, such as a worker that runs customer-written code, a reporting export, or an AI agent acting for one tenant. Scoped keys are for access control only: the [rate limit](https://pozy.ai/docs/getting-started/rate-limits) belongs to the workspace and is shared by every key.

## What it costs

Profiles are free, including empty ones. You pay per connected account per month, counted across the whole workspace on the graduated ladder in [Pricing](https://pozy.ai/docs/billing): the more accounts your customers connect in total, the lower the rate for the extra ones.

To see what each customer costs you, ask for usage grouped by profile:

```bash Terminal
curl "https://api.pozy.ai/v1/usage?range=cycle&groupBy=profile" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

`attribution.groups` has one entry per profile. Their sum plus `attribution.unattributed` (workspace-wide items such as the free credit and 10DLC fees) equals the totals ([usage](https://pozy.ai/docs/billing/usage)).

> **One workspace, one bill:**
> 
> Profiles separate your customers' data and access, but billing, the rate limit and API access belong to your workspace. If the workspace is paused for an unpaid invoice, every profile and every key stops together.

## Offboard a customer

1. Disconnect each of their accounts with `DELETE /v1/accounts/{accountId}`. An account stops counting toward your bill from the next UTC day.
2. Delete the profile with `DELETE /v1/profiles/{profileId}`. A profile that still has active connected accounts cannot be deleted, so step 1 comes first.
3. Delete or revoke any key you scoped to that profile.

A disconnected account can be reconnected into the same profile within one hour and keep its `accountId`. After that it is removed, scheduled posts that only targeted it are cancelled, and a later connection gets a new id.

## Checklist

- Create the profile with `externalId` at signup, store `profile._id`.
- Connect with the customer's `profileId` and your `redirect_url`.
- Handle `account.connected` and `account.disconnected`.
- Send `Idempotency-Key` on every create, derived from your own ids.
- Route webhooks on `externalId`.
- Watch per-customer cost with `GET /v1/usage?groupBy=profile`.
