Profile Groups API Reference
Profile Groups are organizational containers that group related social media profiles together. For example, you might have separate profile groups for different brands, clients, or projects.
For multi-tenant setups, pair a new group with a key scoped to it via the API Keys endpoints — a two-call tenant onboarding at signup.
Endpoints
Section titled “Endpoints”| Method | Endpoint | Description |
|---|---|---|
GET | /api/profile_groups | List all profile groups |
GET | /api/profile_groups/:id | Get a single profile group |
POST | /api/profile_groups | Create a new profile group |
DELETE | /api/profile_groups/:id | Delete a profile group |
POST | /api/profile_groups/:id/initialize_connection | Get OAuth URL to connect a profile |
Profile group object
Section titled “Profile group object”| Field | Type | Description |
|---|---|---|
id | string | Unique profile group identifier (id) |
name | string | Display name of the profile group |
profiles_count | integer | Number of connected profiles in this group |
List profile groups
Section titled “List profile groups”GET /api/profile_groups
Retrieves all profile groups accessible with your API key.
API key behavior
Section titled “API key behavior”| API Key Type | Returns |
|---|---|
| Full account access | All profile groups in the account |
| Profile group scoped | Only the scoped profile group |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const groups = await client.profileGroups.list();console.log(groups);from postproxy import PostProxy
client = PostProxy("YOUR_API_KEY")groups = await client.profile_groups.list()print(groups)package main
import ( "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.New("YOUR_API_KEY") groups, _ := client.ProfileGroups.List() fmt.Println(groups)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")groups = client.profile_groups.listputs groupsuse PostProxy\PostProxy;
$client = new PostProxy("YOUR_API_KEY");$groups = $client->profileGroups->list();print_r($groups);import dev.postproxy.PostProxy;
PostProxy client = new PostProxy("YOUR_API_KEY");var groups = client.profileGroups().list();System.out.println(groups);using PostProxy;
var client = new PostProxyClient("YOUR_API_KEY");var groups = await client.ProfileGroups.ListAsync();Console.WriteLine(groups);Response:
{ "data": [ { "id": "grp123abc", "name": "Main Brand", "profiles_count": 5 }, { "id": "grp456def", "name": "Client Project", "profiles_count": 3 }, { "id": "grp789ghi", "name": "Personal", "profiles_count": 2 } ]}Get profile group
Section titled “Get profile group”GET /api/profile_groups/:id
Retrieves a single profile group by its ID.
Path parameters
Section titled “Path parameters”| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Profile group id |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/profile_groups/grp123abc" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const group = await client.profileGroups.get("grp123abc");console.log(group);import requests
response = requests.get( "https://api.postproxy.dev/api/profile_groups/grp123abc", headers={"Authorization": "Bearer YOUR_API_KEY"})
print(response.json())package main
import ( "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.New("YOUR_API_KEY") group, _ := client.ProfileGroups.Get("grp123abc") fmt.Println(group)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")group = client.profile_groups.get("grp123abc")puts groupuse PostProxy\PostProxy;
$client = new PostProxy("YOUR_API_KEY");$group = $client->profileGroups->get("grp123abc");print_r($group);import dev.postproxy.PostProxy;
PostProxy client = new PostProxy("YOUR_API_KEY");var group = client.profileGroups().get("grp123abc");System.out.println(group);using PostProxy;
var client = new PostProxyClient("YOUR_API_KEY");var group = await client.ProfileGroups.GetAsync("grp123abc");Console.WriteLine(group);Response:
{ "id": "grp123abc", "name": "Main Brand", "profiles_count": 5}Create profile group
Section titled “Create profile group”POST /api/profile_groups
Creates a new profile group.
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
profile_group[name] | string | Yes | Name for the new profile group |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "profile_group": { "name": "New Client Project" } }'import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const group = await client.profileGroups.create("New Client Project");console.log(group);from postproxy import PostProxy
client = PostProxy("YOUR_API_KEY")group = await client.profile_groups.create("New Client Project")print(group)package main
import ( "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.New("YOUR_API_KEY") group, _ := client.ProfileGroups.Create("New Client Project") fmt.Println(group)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")group = client.profile_groups.create("New Client Project")puts groupuse PostProxy\PostProxy;
$client = new PostProxy("YOUR_API_KEY");$group = $client->profileGroups->create("New Client Project");print_r($group);import dev.postproxy.PostProxy;
PostProxy client = new PostProxy("YOUR_API_KEY");var group = client.profileGroups().create("New Client Project");System.out.println(group);using PostProxy;
var client = new PostProxyClient("YOUR_API_KEY");var group = await client.ProfileGroups.CreateAsync("New Client Project");Console.WriteLine(group);Response (201 Created):
{ "id": "grp999xyz", "name": "New Client Project", "profiles_count": 0}Error responses
Section titled “Error responses”Permission denied (scoped API key) (422):
{ "error": "Your API key has no such permission"}Delete profile group
Section titled “Delete profile group”DELETE /api/profile_groups/:id
Deletes a profile group and all its associated profiles.
Path parameters
Section titled “Path parameters”| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Profile group id |
Example
Section titled “Example”curl -X DELETE "https://api.postproxy.dev/api/profile_groups/grp123abc" \ -H "Authorization: Bearer YOUR_API_KEY"import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const result = await client.profileGroups.delete("grp123abc");console.log(result);from postproxy import PostProxy
client = PostProxy("YOUR_API_KEY")result = await client.profile_groups.delete("grp123abc")print(result)package main
import ( "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.New("YOUR_API_KEY") result, _ := client.ProfileGroups.Delete("grp123abc") fmt.Println(result)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")result = client.profile_groups.delete("grp123abc")puts resultuse PostProxy\PostProxy;
$client = new PostProxy("YOUR_API_KEY");$result = $client->profileGroups->delete("grp123abc");print_r($result);import dev.postproxy.PostProxy;
PostProxy client = new PostProxy("YOUR_API_KEY");var result = client.profileGroups().delete("grp123abc");System.out.println(result);using PostProxy;
var client = new PostProxyClient("YOUR_API_KEY");var result = await client.ProfileGroups.DeleteAsync("grp123abc");Console.WriteLine(result);Response:
{ "deleted": true}Initialize connection
Section titled “Initialize connection”POST /api/profile_groups/:id/initialize_connection
Generates a URL to connect a new social media profile to a profile group. This is used to initiate the OAuth flow for connecting social accounts as if it was inside your service.
Path parameters
Section titled “Path parameters”| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Profile group id |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | Platform to connect |
redirect_url | string | Yes (except bluesky, telegram) | URL to redirect to after OAuth completes |
refresh_connection | boolean | No | Re-authorize a profile already connected to this group — to renew tokens or re-pick pages. Skips the “already connected” check for that platform: the same account is updated in place, while a different account is still rejected with account_is_already_connected |
identifier | string | Yes for bluesky | Bluesky handle (e.g. yourname.bsky.social) |
app_password | string | Yes for bluesky | Bluesky app password (generate at bsky.app/settings/app-passwords) |
bot_token | string | Yes for telegram | Telegram bot token from @BotFather |
onboarding | string | No (whatsapp and instagram only) | whatsapp: where the number lives — business_app (coexistence with the WhatsApp Business app) or api (Cloud API or a fresh number); omit to let the user choose on the connect page, see Connect WhatsApp. instagram: which login to use — instagram (default) or facebook, see Connect Instagram |
Supported platforms
Section titled “Supported platforms”| Platform | Account type | Auth type |
|---|---|---|
facebook | Facebook Page | OAuth |
instagram | Instagram Business/Creator Account | OAuth (Instagram or Facebook Login, optional onboarding) |
tiktok | TikTok Account | OAuth |
linkedin | LinkedIn Profile | OAuth |
youtube | YouTube Channel | OAuth |
twitter | X (Twitter) Account | OAuth |
threads | Threads Account | OAuth |
pinterest | Pinterest Account | OAuth |
bluesky | Bluesky Account | App password (no OAuth) |
telegram | Telegram Channels (via bot) | Bring-your-own-bot (no OAuth) |
whatsapp | WhatsApp Business Account | OAuth (Meta Embedded Signup, optional onboarding) |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups/grp123abc/initialize_connection" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "redirect_url": "https://myapp.com/oauth/callback" }'import PostProxy from "postproxy-sdk";
const client = new PostProxy("YOUR_API_KEY");const connection = await client.profileGroups.initializeConnection("grp123abc", "instagram", "https://myapp.com/oauth/callback");console.log(connection);from postproxy import PostProxy
client = PostProxy("YOUR_API_KEY")connection = await client.profile_groups.initialize_connection( "grp123abc", "instagram", "https://myapp.com/oauth/callback")print(connection)package main
import ( "fmt" postproxy "github.com/postproxy/postproxy-go")
func main() { client := postproxy.New("YOUR_API_KEY") connection, _ := client.ProfileGroups.InitializeConnection("grp123abc", "instagram", "https://myapp.com/oauth/callback") fmt.Println(connection)}require "postproxy"
client = PostProxy::Client.new("YOUR_API_KEY")connection = client.profile_groups.initialize_connection( "grp123abc", platform: "instagram", redirect_url: "https://myapp.com/oauth/callback")puts connectionuse PostProxy\PostProxy;
$client = new PostProxy("YOUR_API_KEY");$connection = $client->profileGroups->initializeConnection("grp123abc", "instagram", "https://myapp.com/oauth/callback");print_r($connection);import dev.postproxy.PostProxy;
PostProxy client = new PostProxy("YOUR_API_KEY");var connection = client.profileGroups().initializeConnection("grp123abc", "instagram", "https://myapp.com/oauth/callback");System.out.println(connection);using PostProxy;
var client = new PostProxyClient("YOUR_API_KEY");var connection = await client.ProfileGroups.InitializeConnectionAsync("grp123abc", "instagram", "https://myapp.com/oauth/callback");Console.WriteLine(connection);Response:
{ "url": "https://app.postproxy.dev/partner_connect/inv789xyz", "success": true}Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
url | string | URL to redirect the user to for OAuth authentication |
success | boolean | Whether the invitation was created successfully |
OAuth flow
Section titled “OAuth flow”- Call this endpoint to get the connection URL
- Redirect the user to the returned
url - User authenticates with the social platform
- User is redirected to your
redirect_urlafter completion, withid(the new profile’s id) andplatform_id(the account’s ID on the platform) appended as query parameters - A new profile is created in the specified profile group
Example: If your redirect_url is https://myapp.com/callback?org=123, a successful connection redirects to https://myapp.com/callback?org=123&id=prof123abc&platform_id=123456789012345.
If the user cancels or the OAuth flow fails, the user is redirected to your redirect_url with ?failure=true and an error_code appended as query parameters. Any existing query parameters on your redirect_url are preserved.
| Error Code | Description |
|---|---|
user_abandoned | The user cancelled or dismissed the OAuth flow |
account_is_already_connected | The social account is already connected to another profile. platform_id is appended alongside failure and error_code, so you can tell which platform account the user tried to connect |
update_failed | Failed to update a previously disconnected profile during reconnection |
Example: If your redirect_url is https://myapp.com/callback?org=123, a cancelled connection redirects to https://myapp.com/callback?org=123&failure=true&error_code=user_abandoned.
Error responses
Section titled “Error responses”Missing redirect_url (422):
{ "error": "Missing redirect_url"}Missing platform (422):
{ "error": "Missing platform"}Platform already connected (422):
{ "error": "Platform already connected"}Connect Instagram (Instagram or Facebook Login)
Section titled “Connect Instagram (Instagram or Facebook Login)”An Instagram professional (Business or Creator) account can be connected two ways. Instagram Login is the default and works for any professional account. Facebook Login works for an account that is linked to a Facebook Page the user manages, and is the route to pick when the user administers the account from Facebook.
POST /api/profile_groups/:id/initialize_connection
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | instagram |
redirect_url | string | Yes | URL to redirect to after OAuth completes |
onboarding | string | No | instagram (default) or facebook |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups/grp123abc/initialize_connection" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "onboarding": "facebook", "redirect_url": "https://myapp.com/oauth/callback" }'Facebook login connects every Instagram account linked to the Pages the user grants, not just one. The id and platform_id appended to your redirect_url belong to the primary account: the one already active in this group, otherwise the first one not connected elsewhere. The rest are connected at the same time:
- On plans billed per profile, they are all active in this group.
- On group-billed plans a group holds one Instagram profile, so the others are created with
status: "inactive"andinactive_reason: "network_taken". They don’t sync and fire no webhooks until you move each one to a group without an Instagram profile, which activates it and firesprofile.connected. Find them withGET /api/profiles. - Accounts already connected in another group stay in that group and get fresh tokens.
- With
refresh_connection, a different account isn’t rejected — it’s added the same way.
Calls without onboarding behave exactly as before. Either login produces a profile with platform: "instagram", and the same API everywhere afterwards. Connecting an account again with the other login method updates the existing profile in place, keeping its id, post history and billing.
The connect page the url opens offers both routes as Continue with Instagram and Continue with Facebook, and an “Ask colleague” link gives the user both options as well.
Connect WhatsApp (Embedded Signup)
Section titled “Connect WhatsApp (Embedded Signup)”WhatsApp connects through Meta’s Embedded Signup — a Facebook login that grants Postproxy access to a WhatsApp Business Account (WABA) and its phone numbers. The call returns a url like any OAuth platform; what’s different is the onboarding parameter.
POST /api/profile_groups/:id/initialize_connection
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | whatsapp |
redirect_url | string | Yes | URL to redirect to after signup completes |
onboarding | string | No | business_app, api, or omitted — see below |
Choosing onboarding
Section titled “Choosing onboarding”| Value | Use when | What happens |
|---|---|---|
business_app | The number is in the WhatsApp Business app on a phone and the team answers customers from it | Coexistence. Meta shows a QR code the user scans from WhatsApp Business (Settings → Linked devices). The phone keeps working, Postproxy sees the same chats, and up to 6 months of history is imported |
api | The number is already on the Cloud API, or is a fresh number that will only be used from Postproxy | Standard Cloud API onboarding. The number is claimed for the API and stops working in the WhatsApp app. A new number is verified by SMS or voice call during signup |
| (omitted) | You don’t know which applies | The connect page opens with a “Where is your number now?” chooser before the Facebook login |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups/grp123abc/initialize_connection" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "whatsapp", "redirect_url": "https://myapp.com/oauth/callback", "onboarding": "api" }'What gets created
Section titled “What gets created”One connected WABA becomes one profile, and each of its phone numbers becomes a placement of that profile. If the login grants several WABAs, one profile is created per WABA.
Numbers arrive asynchronously: profile.placements_synced fires once they’re ready, then GET /api/profiles/:id/placements lists them with their display_phone_number, quality_rating and messaging_limit_tier. Poll or wait for the webhook before sending.
WhatsApp profiles are messaging-only — POST /api/posts returns 422. Full details, including coexistence limits and the message template API, are on the WhatsApp platform page.
Connect Bluesky (App Password)
Section titled “Connect Bluesky (App Password)”Bluesky does not support OAuth in the same way other platforms do. Instead of returning a redirect URL, the API authenticates synchronously with the user’s Bluesky handle and an app password, then creates the profile in one call.
POST /api/profile_groups/:id/initialize_connection
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | Must be "bluesky" |
identifier | string | Yes | Bluesky handle. The .bsky.social suffix is added automatically if missing. Custom domains (e.g. you.example.com) are accepted as-is. |
app_password | string | Yes | Bluesky app password, not the account password. Users generate one at https://bsky.app/settings/app-passwords |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups/grp123abc/initialize_connection" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "platform": "bluesky", "identifier": "yourname.bsky.social", "app_password": "xxxx-xxxx-xxxx-xxxx" }'Response:
{ "success": true, "profile": { "id": "prof321zyx", "network": "bluesky", "name": "Your Display Name", "external_username": "yourname.bsky.social" }}Error responses
Section titled “Error responses”Missing credentials (422):
{ "error": "Bluesky requires `identifier` (handle) and `app_password` in the payload. Generate an app password at https://bsky.app/settings/app-passwords", "error_code": "missing_credentials"}Invalid credentials / 2FA required (401):
{ "error": "This account requires 2FA. Generate an app password at bsky.app/settings/app-passwords and use that instead of your main password.", "error_code": "bluesky_login_failed"}Account already connected to another profile group (422):
{ "error": "This Bluesky profile is already connected to Postproxy.", "error_code": "account_is_already_connected"}Connect Telegram (Bring Your Own Bot)
Section titled “Connect Telegram (Bring Your Own Bot)”Telegram does not expose consumer OAuth for posting. Instead, the user creates their own bot via @BotFather and adds it as administrator to the channel(s) they want to publish to. Postproxy uses that bot’s token to publish.
A single Telegram Profile represents one bot. The channels it can publish to are exposed via the placements endpoint, and the destination channel for each post is selected per-post via the chat_id parameter — see platform parameters.
For a step-by-step walkthrough (creating the bot, adding it to channels, publishing the first post), see the Connect Telegram with your own bot guide.
Three-step flow
Section titled “Three-step flow”- Submit the bot token with this endpoint to create a
Profile. Postproxy validates the token and registers a webhook with Telegram. - User adds the bot as administrator to each Telegram channel they want to publish to. As Telegram notifies us via the webhook, channels appear in the Profile’s placements.
- Poll
GET /api/profiles/:id/placementsto list discovered channels. Usechat_id(the placement’sid) when creating posts.
POST /api/profile_groups/:id/initialize_connection
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | Must be "telegram" |
bot_token | string | Yes | Token issued by @BotFather. Get one with /newbot, then choose a name and username and copy the token. |
Example
Section titled “Example”curl -X POST "https://api.postproxy.dev/api/profile_groups/grp123abc/initialize_connection" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "platform": "telegram", "bot_token": "123456789:ABCdef-GhIJklMnOpQrStUvWxYz" }'Response:
{ "success": true, "profile": { "id": "prof321zyx", "network": "telegram", "name": "My Bot", "external_username": "my_bot" }, "next_step": "Add @my_bot as administrator to the Telegram channel(s) you want to publish to. Channels will appear in GET /api/profiles/prof321zyx/placements once Telegram notifies us."}Listing channels
Section titled “Listing channels”After the bot has been added as administrator to one or more channels, list them via:
curl "https://api.postproxy.dev/api/profiles/prof321zyx/placements" \ -H "Authorization: Bearer your_api_key"{ "data": [ { "id": "-1001234567890", "name": "My Channel (@mychannel)" }, { "id": "-1009876543210", "name": "Private Channel" } ]}The list is empty until the user adds the bot to a channel. We recommend polling every few seconds while the user completes that step. If a channel never appears, ask the user to remove and re-add the bot — that re-fires the discovery event.
Error responses
Section titled “Error responses”Missing bot token (422):
{ "error": "Telegram requires `bot_token` in the payload. Create a bot via @BotFather (https://t.me/BotFather) and pass the token."}Invalid bot token (401):
{ "error": "Telegram bot token is invalid or revoked — please reconnect the profile.", "error_code": "telegram_connect_failed"}Bot already connected to another profile group (422):
{ "error": "This Telegram bot is already connected to Postproxy.", "error_code": "account_is_already_connected"}Use cases
Section titled “Use cases”Multi-brand management
Section titled “Multi-brand management”Create separate profile groups for different brands:
# Create brand profile groupscurl -X POST "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{"profile_group": {"name": "Brand A"}}'
curl -X POST "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{"profile_group": {"name": "Brand B"}}'Client onboarding
Section titled “Client onboarding”Generate connection URLs for clients to connect their accounts:
# Get OAuth URL for client to connect Instagramcurl -X POST "https://api.postproxy.dev/api/profile_groups/grp_client123/initialize_connection" \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "redirect_url": "https://yourplatform.com/client/connected" }'Scoped API keys for partners
Section titled “Scoped API keys for partners”Issue profile-group-scoped API keys to partners, allowing them access only to their designated profile group while protecting other data.