> ## Documentation Index
> Fetch the complete documentation index at: https://docs.letmepost.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform credentials

> Register the developer apps that let your self-hosted instance talk to each social network.

The hosted SaaS at `api.letmepost.dev` ships with platform-approved OAuth apps for every network we support. When you self-host, you bring your own — each platform requires that the OAuth client id match an app you control, registered against your own developer account.

Bluesky is the exception: it uses [app passwords](https://bsky.app/settings/app-passwords) and doesn't need a developer app at all.

For each platform below: register the app, configure the redirect URI exactly as listed (substituting your own `${BETTER_AUTH_URL}` for the placeholder), paste the resulting client id + secret into your `.env`, and restart the API.

<Note>
  Throughout this page, `${BETTER_AUTH_URL}` means whatever you set [`BETTER_AUTH_URL`](/self-host/environment#http-server) to — typically `http://localhost:3000` in dev or `https://api.letmepost.dev` in prod. The redirect URI must match the platform's record character-for-character, including trailing slash (or lack thereof).
</Note>

## LinkedIn

* **Portal:** [linkedin.com/developers/apps](https://www.linkedin.com/developers/apps)
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/linkedin/callback`
* **Env vars:** `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`

Enable these two products on the app:

* **Sign In with LinkedIn using OpenID Connect** — gives `openid`, `profile`.
* **Share on LinkedIn** — gives `w_member_social`.

letmepost requests these scopes:

* **Write:** `openid`, `profile`, `w_member_social`
* **Extended:** `email`, `r_organization_social`, `w_organization_social`

Personal posting works out of the box. Org / Company-Page posting (`w_organization_social`) requires LinkedIn's **Marketing Developer Platform (MDP)** review — separate, slower process. The personal-posting flow doesn't need MDP.

## Twitter / X

* **Portal:** [developer.x.com](https://developer.x.com/en/portal/dashboard)
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/twitter/callback`
* **Env vars:** `TWITTER_CLIENT_ID`, `TWITTER_CLIENT_SECRET`

OAuth 2.0 settings on the app:

* **Type of App:** Web App, Automated App or Bot
* **Authentication:** OAuth 2.0 with PKCE (Confidential client)
* **Scopes (write):** `tweet.read`, `tweet.write`, `users.read`, `offline.access`
* **Scopes (extended):** `like.read`, `follows.read`

As of Feb 2026 X killed tiered pricing for new signups — Basic / Pro tiers are closed; pay-per-use is the only option for new developers. Posting requires write access on the paid tier.

Optional billing safety net: `TWITTER_LAUNCH_CAP_PER_ACCOUNT` (default `50`) caps billable posts per rolling 30-day window per account, so an uncapped publish loop can't run up the PPU bill.

## Pinterest

* **Portal:** [developers.pinterest.com/apps](https://developers.pinterest.com/apps/)
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/pinterest/callback`
* **Env vars:** `PINTEREST_CLIENT_ID`, `PINTEREST_CLIENT_SECRET`

Scopes:

* **Write:** `boards:read`, `boards:write`, `pins:read`, `pins:write`, `user_accounts:read`
* **Extended:** `pins:read_secret`

New apps start in **Trial Access** — pin creation on production (`api.pinterest.com`) is blocked with a hard "use sandbox" error until Pinterest approves Standard Access. While you wait:

```bash theme={"system"}
PINTEREST_API_BASE=https://api-sandbox.pinterest.com/v5
```

Clear `PINTEREST_API_BASE` once Standard Access is approved — it defaults back to `https://api.pinterest.com/v5`. The variable affects every `/v5/*` call *and* the OAuth token-exchange endpoint, since both live under the same host on Pinterest's API.

## Meta family

The Meta family used to be a single Facebook Login for Business app that fanned out to Facebook Pages, Instagram, and Threads. In the current build, **each of those three is an independent OAuth flow** with its own client credentials.

You still register all three on [developers.facebook.com/apps](https://developers.facebook.com/apps/) — Meta's developer portal — but the apps, scopes, redirect URIs, and review tracks are separate.

### Facebook Pages

* **Portal:** [developers.facebook.com/apps](https://developers.facebook.com/apps/)
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/facebook/callback`
* **Env vars:** `META_APP_ID`, `META_APP_SECRET`

Products to add on the app:

* **Facebook Login for Business**

Scopes letmepost requests (write):

```
pages_manage_posts
pages_read_engagement
pages_show_list
business_management
```

<Note>
  The redirect URI ends in `/facebook/callback`, not `/meta/callback`. Earlier docs incorrectly said `/meta/callback`; if your Meta app has that registered, update it.
</Note>

App Review takes 2–8 weeks per cycle and rejections are normal. Build against Development Mode + Tester accounts during the wait — same Graph API endpoints, same response shapes; review only lifts the testers-only gate. Business verification is required before submission.

Optional: `META_GRAPH_VERSION` pins the Graph API version (the provider has a default; bump it on each platform version cut and re-run the contract test suite).

### Instagram (Instagram API with Instagram Login)

* **Portal:** [developers.facebook.com/apps](https://developers.facebook.com/apps/) — same portal as Facebook, but you'll enable the **Instagram** product → **"Instagram API setup with Instagram Login"** path.
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/instagram/callback`
* **Env vars:** `INSTAGRAM_CLIENT_ID`, `INSTAGRAM_CLIENT_SECRET`

Scopes letmepost requests (write):

```
instagram_business_basic
instagram_business_content_publish
```

This is **different** from the Instagram fan-out you used to get through Facebook Login for Business. Instagram Login is for users who want to connect with just their Instagram account — **Professional accounts only (Business or Creator)**, no Facebook Page required. The token is bound to the IG user and publishes hit `graph.instagram.com` instead of `graph.facebook.com`.

App Review is separate from the Facebook Login scopes — different scope namespace, different review cycle.

The optional `INSTAGRAM_GRAPH_BASE`, `INSTAGRAM_OAUTH_AUTHORIZE_URL`, `INSTAGRAM_OAUTH_TOKEN_URL` variables are test-only — they let the contract suite point at a mock server. Leave them blank in production.

### Threads

* **Portal:** [developers.facebook.com/apps](https://developers.facebook.com/apps/) — add the **Threads API** product.
* **Authorize URL:** `https://threads.net/oauth/authorize` (NOT `facebook.com/dialog/oauth`)
* **Redirect URI:** `${BETTER_AUTH_URL}/v1/accounts/oauth/threads/callback`
* **Env vars:** `THREADS_CLIENT_ID`, `THREADS_CLIENT_SECRET`

Scopes:

* **Write:** `threads_basic`, `threads_content_publish`
* **Extended:** `threads_manage_replies`, `threads_read_replies`

Threads has its **own OAuth flow + client credentials**, separate from Facebook Login for Business. The Threads API product on developers.facebook.com generates a distinct `THREADS_CLIENT_ID` / `THREADS_CLIENT_SECRET` — do not reuse `META_APP_ID`.

Tokens are short-lived (1h) on first exchange, swapped server-side for 60-day long-lived tokens which are what letmepost persists.

App Review for `threads_content_publish` is required for production; Dev Mode with Tester accounts is fully functional in the meantime — same API, same response shapes, review only lifts the testers-only gate.

Optional: `THREADS_API_BASE`, `THREADS_API_VERSION` let tests point at a mock server. Defaults: `https://graph.threads.net`.

## Bluesky

No registration. No client id. No secret. Bluesky's documented integration path for third-party tools is the user-generated **app password** flow.

The user creates an app password at [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords), pastes it into the dashboard's Bluesky connect form alongside their handle, and the API stores it encrypted with `KEK_MASTER`. There's nothing to configure on a self-hosted instance — connecting Bluesky works the moment the API can reach `bsky.social`.

## Verifying a connection

Once you've set credentials and restarted the API, the connect flow runs from the dashboard at `/accounts`. Pick the platform, complete OAuth at the provider, land back on the dashboard with a connected account row.

If OAuth fails, the error envelope includes a stable `code` and a `docUrl` pointing at the troubleshooting page — see [`platform_auth_failed`](/errors/platform_auth_failed) for the most common cause (a redirect URI that doesn't match the platform's record character-for-character).
