For SaaS products

Let your users connect
their social accounts.

Upload-Post and Ayrshare call this a JWT or user profile link. You create a bucket per customer, hand them a signed URL, they approve access on the network's screen, then your backend publishes. PostLake's version is POST /v1/connect-link with profile and returnUrl. The hosted page and the OAuth consent still say PostLake unless you bring your own platform app credentials (BYOK). That BYOK step is white-label OAuth consent, not a separate product. Do not mint POST /v1/app-link for end users: that is a 12-hour dashboard session for you, not a connect flow for them.

Get started free →

Free tier. No card · one API for every network · full MCP access

In shortOne PostLake workspace, one profile per end user, a signed connect link with returnUrl, then publish with acc_ ids from your backend. Rate limits are per profile, with a workspace ceiling. BYOK is how the OAuth consent screen shows your app name.

When this guide is for you

You are building a product whose users connect their own social accounts, and you want PostLake to run OAuth, publishing, and analytics instead of holding nine platform apps yourself.

Before you start

Same setup the docs quickstart uses. Do this once:

  1. Sign up at app.postlake.dev and verify your email (unlocks free credits).
  2. On Channels, create a profile (e.g. my-brand) and connect at least one account. Bluesky is the fastest first channel: no app review. Instagram/TikTok/Facebook need each platform's review before API posting.
  3. Account menu → API Keys → create a key (sk_live_…). It shows once; treat it like a password.
  4. You do not connect your customers' networks in Channels. Each customer connects through the signed link you mint.

Working example

One PostLake key on your backend. One profile per customer. Then this:

import { PostLake } from "postlake";

const pl = new PostLake(process.env.POSTLAKE_API_KEY);

// 1. One profile per end user (the tenant bucket).
const profile = await pl.profiles.create({ name: "user-" + userId });

// 2. Mint a 30-minute connect link. After OAuth they land on returnUrl.
const { url } = await pl.connectLink({
  profile: profile.username,
  returnUrl: "https://yourapp.example/social-connected",
  platforms: ["instagram", "tiktok", "linkedin"],
});
// Send `url` to the browser. Never send your API key.

// 3. On returnUrl, read connect_status=success|cancelled|error.
//    Then list that profile's channels and store the acc_ ids.
const { data: accounts } = await pl.socialAccounts.list({ profile: profile.username });

// 4. Publish from your backend. Credits meter successful publishes.
const post = await pl.posts.create({
  text: "Shipped in our app",
  accounts: accounts.map((a) => a.id),
});

// 5. Analytics and disconnect stay scoped to the same profile.
await pl.analytics.get({ period: "30d", profile: profile.username });
await pl.socialAccounts.disconnect(accounts[0].id);

Make it work in the API

Tool-specific glue on top of the shared setup above:

  1. Keep one PostLake account for your company. Create a profile per end user. That is the isolation boundary.
  2. From your backend, POST /v1/connect-link with an existing profile and a returnUrl (https, or http on localhost). An unknown profile returns 404 rather than creating an unscoped flow. Optionally pass platforms so the hosted page only shows the networks you support.
  3. Open the returned 30-minute URL in the user's browser. PostLake returns them to your app with connect_status=success|cancelled|error, plus platform and profile. Error outcomes also include a stable error_code for your UI and logs.
  4. Subscribe to account.connected and account.disconnected. List GET /v1/social-accounts?profile= and store the acc_… ids against that user.
  5. Publish with POST /v1/posts and accounts: [acc_…]. Read GET /v1/analytics?profile= so one customer's numbers do not mix with another's.
  6. When they disconnect in your UI, DELETE /v1/social-accounts/{id}. Do not hand them POST /v1/app-link.
  7. Read GET /v1/me/limits for both envelopes. Free and credit packs share 120 API calls and 12 publishes per minute per profile, with a workspace ceiling of 2,400 API calls and 180 publishes per minute. Paid plans raise both. Pass profile (or post with that profile's acc_ ids) so one customer cannot spend another customer's headroom. Networks still apply their own caps.

Read the response (don't skip this)

You get one Post with an overall state and a targets[] array. One entry per account. Always check each target; partial success is normal.

{
  "id": "post_a1b2c3",
  "state": "partial",
  "targets": [
    { "platform": "bluesky",  "state": "published", "url": "https://bsky.app/…" },
    { "platform": "linkedin", "state": "failed",
      "error": { "type": "invalid_request", "message": "…", "retryable": false } }
  ]
}

Where the post goes

Same rules as the docs. Pick one addressing style:

See Publishing: where to post.

Do more (same API)

Pitfalls specific to this path

Prefer an assistant over the API code? Point it at MCP (https://api.postlake.dev/mcp). Prefer the dashboard? Use Quickstart → No code.

Common questions

Is this a white-label social media API?

The publishing API is yours to wrap. Your users stay in your product except for two screens: PostLake's hosted connect page, and the network's OAuth consent. White-label consent (Instagram showing your app name, not PostLake) needs your own platform credentials via POST /v1/credentials. The hosted connect page stays PostLake-branded either way.

Can my users connect social accounts without seeing PostLake?

They will see PostLake's hosted connect page, then the network's own consent screen. The consent screen shows your app name only if you bring your own platform credentials (BYOK). There is no separate publishable key to embed in a browser.

Are connected accounts unlimited on PAYG?

Yes. Credit packs do not change the plan. Connections are unlimited on Free and every paid plan. Credits meter successful publishes, not connections.

Are rate limits per user or per my PostLake account?

Both. GET /v1/me/limits returns workspace apiPerMinute and postsPerMinute, plus rateLimits.profile for one Channels profile. Free and credit-pack accounts get 120 API calls and 12 publishes per minute per profile, and a workspace ceiling of 2,400 / 180. Pass profile on reads and publishes so users are isolated. Ayrshare's 300 calls / 5 minutes is the same idea, per profile.

Is analytics included if we only buy credit packs?

Yes. Analytics is not gated by plan. Pass profile on GET /v1/analytics so each customer's rollup stays in their bucket.

Go deeper in the docs

These guides stay short on purpose. Canonical behaviour lives here:

Also: Media · Errors · MCP · Analytics

Related guides

All guides · Full docs · llms.txt · Markdown

Stuck? The docs are the source of truth, start at Publishing.

Open the dashboard →