Profiles and accounts
A profile is a brand or one of your end users. An account is one social account connected to a profile, like an Instagram business account or a Bluesky handle. Posts go to accounts.
Profiles
If you publish for one brand, one profile is enough. If your product publishes for your customers, create a profile per customer and put your own id in external_id; it must be unique across your profiles.
POST/v1/profiles
| Field | Description |
|---|---|
namestring, 1–100 · required | Shown in the dashboard. |
external_idstring, 1–200 | Your id for this brand or user. A duplicate gets 409 conflict. |
curl https://api.postpeg.com/v1/profiles \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Coffee",
"external_id": "customer_4821"
}'{
"id": "prof_mfz0b7q2k9x4c1v8n3m6a0s5d2",
"name": "Acme Coffee",
"external_id": "customer_4821",
"created_at": "2026-09-24T13:58:40.021Z"
}GET /v1/profiles lists them, GET /v1/profiles/{id} fetches one, and DELETE /v1/profiles/{id} deletes one (204). Disconnect a profile’s accounts before deleting it, or you get a 409.
Connecting an account
Accounts on every network except Bluesky connect through a hosted OAuth flow: you ask for a link, send the account’s owner to it, they approve on the network’s own screen, and they come back to you.
POST/v1/accounts/connect
| Field | Description |
|---|---|
profile_idstring · required | The profile the account will belong to. |
platformstring · required | One of x, instagram, facebook, linkedin, tiktok, youtube, threads, pinterest, google_business. |
redirect_urlhttp(s) URL · required | Where the owner lands when they’re done, successful or not. |
curl https://api.postpeg.com/v1/accounts/connect \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile_id": "prof_mfz0b7q2k9x4c1v8n3m6a0s5d2",
"platform": "instagram",
"redirect_url": "https://example.com/settings/social"
}'{
"url": "https://…",
"expires_at": "2026-09-24T14:15:12.000Z"
}The link works for 15 minutes; after that the owner sees an expiry message and you ask for a new one. After the network’s own sign-in screen, any choice the network needs (which Facebook Page, whether to post to LinkedIn as a person or a company page, which Pinterest board, which Google Business location) is made on a postpeg page. When they finish, the browser goes to your redirect_url with the result in the query string:
# Connected
?status=success&account_id=acc_mfz20b5n1s8f4j7xv2p9r6hu1c&platform=instagram
# Not connected
?status=error&error=account_limit&error_description=The+plan%E2%80%99s+connected-account+limit+is+reached...&platform=instagramOn success, fetch the account with GET /v1/accounts/{account_id} rather than trusting the query string alone. On error, error is a stable code and error_description is a sentence you can show the owner as-is. Common codes:
account_limit: your plan is full. The account was not kept.cancelledoroauth_denied: the owner cancelled, on our page or on the network’s sign-in screen.personal_account_not_supported: the network needs a business or creator account.connection_failed,account_not_found: the flow didn’t complete. Ask the owner to try again.- New codes may be added; treat an unknown code as a generic failure and show
error_description.
Connecting the same social account to the same profile again doesn’t create a duplicate: it refreshes the existing account and keeps its id.
Bluesky app passwords
Bluesky accounts connect with the handle and an app password, never the account’s real password. The owner creates one in Bluesky under Settings → Privacy and security → App passwords; it looks like xxxx-xxxx-xxxx-xxxx. Anything else is rejected with a 400.
POST/v1/accounts/bluesky
curl https://api.postpeg.com/v1/accounts/bluesky \
-H "Authorization: Bearer $POSTPEG_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile_id": "prof_mfz0b7q2k9x4c1v8n3m6a0s5d2",
"handle": "acmecoffee.bsky.social",
"app_password": "abcd-efgh-ijkl-mnop"
}'{
"id": "acc_mfz21c8w3e6r0t4y2u9i5o1p7a",
"profile_id": "prof_mfz0b7q2k9x4c1v8n3m6a0s5d2",
"platform": "bluesky",
"username": "acmecoffee.bsky.social",
"display_name": "Acme Coffee",
"avatar_url": null,
"provider": "native",
"status": "active",
"connected_at": "2026-09-24T14:00:12.530Z"
}If Bluesky refuses the pair you get a 400 saying “Bluesky rejected that handle and app password”.
Listing accounts
GET/v1/accounts
Every connected account, newest first. Filter with profile_id and platform. Disconnected accounts are not listed.
curl https://api.postpeg.com/v1/accounts?profile_id=prof_mfz0b7q2k9x4c1v8n3m6a0s5d2 \
-H "Authorization: Bearer $POSTPEG_KEY"{
"data": [
{
"id": "acc_mfz21c8w3e6r0t4y2u9i5o1p7a",
"profile_id": "prof_mfz0b7q2k9x4c1v8n3m6a0s5d2",
"platform": "bluesky",
"username": "acmecoffee.bsky.social",
"display_name": "Acme Coffee",
"avatar_url": null,
"provider": "native",
"status": "active",
"connected_at": "2026-09-24T14:00:12.530Z"
}
],
"next_cursor": null
}provider is informational: it says how postpeg reaches that network for the account. The API behaves the same either way, and the account keeps its id if it changes.
Account statuses
| Field | Description |
|---|---|
activestatus | Connected and working. Posts to it are published. |
reconnect_requiredstatus | The network rejected the saved login (it expired or the owner revoked access). New posts to the account are refused with a 400, and due posts fail with “The account must be reconnected”. Run the connect flow again for the same profile and network to fix it; the account keeps its id. |
disconnectedstatus | You disconnected it. It no longer appears in lists or counts to your plan. |
Disconnecting
DELETE/v1/accounts/{id}
Returns 204. Posts still scheduled for the account will fail with “The account was disconnected”; its other targets are unaffected.
curl -X DELETE https://api.postpeg.com/v1/accounts/acc_mfz21c8w3e6r0t4y2u9i5o1p7a \
-H "Authorization: Bearer $POSTPEG_KEY"Plan limits
Each plan caps connected accounts across all profiles: 3 on the trial, 10 on Starter, 50 on Pro, and unlimited on Scale (fair use 150). Reconnecting an account that’s already counted is always allowed. Going over gets a 402 (or error=account_limit on the OAuth redirect).
{
"type": "https://postpeg.com/errors/account_limit",
"title": "Account limit reached",
"status": 402,
"detail": "The Trial plan allows 3 connected accounts. Disconnect one or upgrade.",
"code": "account_limit",
"request_id": "8f14e45f-ceea-467a-9575-1f2a5c3e6b0d"
}See Pricing for the plans.