For SaaS products
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
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.
Same setup the docs quickstart uses. Do this once:
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.sk_live_…). It shows once; treat it like a password.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);Tool-specific glue on top of the shared setup above:
profile per end user. That is the isolation boundary.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.connect_status=success|cancelled|error, plus platform and profile. Error outcomes also include a stable error_code for your UI and logs.account.connected and account.disconnected. List GET /v1/social-accounts?profile= and store the acc_… ids against that user.POST /v1/posts and accounts: [acc_…]. Read GET /v1/analytics?profile= so one customer's numbers do not mix with another's.DELETE /v1/social-accounts/{id}. Do not hand them POST /v1/app-link.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.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 } }
]
}published: every target succeeded. partial: some published, some failed. failed: none published. processing: still going (async networks like TikTok).Idempotency-Key header on writes so a retry never double-posts.Same rules as the docs. Pick one addressing style:
"profile": "my-brand" posts to every account under that profile (the name on Channels)."platforms": ["bluesky", "linkedin"] narrows that set. It is a filter, not a selector: if you have two Pinterest boards, both match pinterest."accounts": ["acc_…"] for exact channels (copy an id on Channels, or GET /v1/social-accounts).profile? PostLake uses that profile. Multiple profiles and you omit it? You'll get an error that names them.See Publishing: where to post.
scheduledAt as UTC (trailing Z), or a naive local time plus timezone (IANA, e.g. Europe/London). Credits charge when it fires. Scheduling docs · scheduling guide.POST /v1/media, then pass the med_… id in media. Media docs.textOverrides (e.g. shorter text for X). Per-network options: platformOptions (Pinterest boardId, TikTok privacy, …). Live option lists: GET /v1/platforms/{platform}.POST /v1/posts/validate runs the same checks without publishing (free).profile can show every channel in your workspace after OAuth. Always mint one scoped to that user.POST /v1/credentials.Prefer an assistant over the API code? Point it at MCP (https://api.postlake.dev/mcp). Prefer the dashboard? Use Quickstart → No code.
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.
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.
Yes. Credit packs do not change the plan. Connections are unlimited on Free and every paid plan. Credits meter successful publishes, not connections.
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.
Yes. Analytics is not gated by plan. Pass profile on GET /v1/analytics so each customer's rollup stays in their bucket.
These guides stay short on purpose. Canonical behaviour lives here:
Also: Media · Errors · MCP · Analytics
All guides · Full docs · llms.txt · Markdown
Stuck? The docs are the source of truth, start at Publishing.
Open the dashboard →