Skip to content
postpeg

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

Create profile fields
FieldDescription
namestring, 1–100 · requiredShown in the dashboard.
external_idstring, 1–200Your 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"
  }'
Response · 201 Created
{
  "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

Connect fields
FieldDescription
profile_idstring · requiredThe profile the account will belong to.
platformstring · requiredOne of x, instagram, facebook, linkedin, tiktok, youtube, threads, pinterest, google_business.
redirect_urlhttp(s) URL · requiredWhere 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"
  }'
Response · 200 OK
{
  "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:

Redirect
# 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=instagram

On 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.
  • cancelled or oauth_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"
  }'
Response · 201 Created
{
  "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"
Response · 200 OK
{
  "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

Account statuses
FieldDescription
activestatusConnected and working. Posts to it are published.
reconnect_requiredstatusThe 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.
disconnectedstatusYou 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).

Response · 402 Payment Required
{
  "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.