PostFast
Configure PostFast with a workspace API key, scheduled publishing, key-based media uploads, post analytics, and hosted connect links.
PostFast is an optional managed backend. It holds the connected accounts in your PostFast workspace and publishes posts at the time you schedule. PostFast sponsors Social SDK; the adapter follows PostFast’s public API the same way the other hosted adapters follow theirs.
import { createSocial } from "@opencoredev/social-sdk";
import { postfast } from "@opencoredev/social-sdk/cloud/postfast";
const social = createSocial({ backend: postfast({ apiKey: process.env.POSTFAST_API_KEY! }) });
const accounts = await social.accounts.list({
backend: "default",
authorization: { tenantId: "tenant-from-session" },
});
The API key belongs to one PostFast workspace. Keep it on your server; the adapter sends it in the pf-api-key header.
Every post is scheduled
PostFast’s API has no publish-now call. Each target needs a schedule.at in the future, and prepare reports schedule.required or schedule.past otherwise:
const result = await social.posts.publish(
{
targets: [{ account }],
content: { text: "Launching next week." },
schedule: { at: "2026-10-01T09:00:00.000Z" },
},
{ authorization: { tenantId: "tenant-from-session" } },
);
A successful submission returns a scheduled outcome whose job ID is the PostFast post ID. If your workspace requires post approval, the outcome is accepted with backend state SCHEDULED/PENDING_APPROVAL until someone approves it in PostFast. Call getDelivery later to see published, failed, or the approval state. PostFast has no webhooks, so poll for status.
The remaining examples pass authorization the same way. Full, type-checked versions are in examples/snippets/backend-postfast.ts.
Poll for status
Store the outcome’s delivery reference and poll it from a job. Keep polling while the post is scheduled, waiting for approval, or processing, and stop on any other state.
import { setTimeout as sleep } from "node:timers/promises";
let outcome = await social.posts.getDelivery(delivery, { authorization });
for (let check = 1; check < 30 && isWaiting(outcome); check++) {
await sleep(60_000);
outcome = await social.posts.getDelivery(delivery, { authorization });
}
function isWaiting(outcome: DeliveryOutcome): boolean {
switch (outcome.state) {
case "scheduled": // due later
case "accepted": // SCHEDULED/PENDING_APPROVAL: waiting for approval in PostFast
case "processing":
return true;
case "published":
case "failed":
case "cancelled":
case "not-submitted":
case "unknown": // reconcile in PostFast before retrying
return false;
}
}
If the submission itself was ambiguous, the outcome is unknown with no delivery. Check PostFast before you publish again.
Media
PostFast references media by storage key, not by URL. The adapter asks PostFast for a signed upload URL, uploads the bytes there without your API key, and sends the key with the post. HTTPS media URLs are rejected at preparation, because the SDK does not fetch remote files for you. Supported types are JPEG, PNG, GIF, and WebP images up to 10 MB, and MP4, WebM, and MOV (QuickTime) videos up to 250 MB. PostFast’s schema has no alt text field, so attachments with altText are rejected.
Pass a Blob in the post, or upload it first with media.upload and publish the returned reference. The reference resolves through the adapter’s mediaStore, which is in memory by default, so upload and publish with the same adapter instance or configure a shared store.
const media = await social.media.upload(
{
kind: "video",
source: { kind: "blob", blob: video, fingerprint: "product-demo-v1" },
mimeType: "video/mp4",
filename: "product-demo.mp4",
},
youtubeAccount,
{ authorization },
);
await social.posts.publish(
{
targets: [
{
account: youtubeAccount,
options: { title: "Product demo", visibility: "public", madeForKids: false },
},
],
content: {
text: "A short demo of the new editor.",
media: [{ kind: "video", source: { kind: "media-ref", ref: media }, mimeType: "video/mp4" }],
},
schedule: { at },
},
{ authorization },
);
PostFast documents Bluesky support for text and images only, so a Bluesky target with a video is rejected at preparation.
Supported operations
The adapter supports account listing, scheduled publishing, delivery status, cancelling a future scheduled post, deleting a failed backend record, media upload, and post analytics. Cancelling deletes the PostFast record. Neither operation removes a post that already published.
if (outcome.state === "scheduled") {
const { backendRecord } = await social.posts.cancelScheduled(outcome.job, { authorization });
}
if (outcome.state === "failed" && outcome.delivery) {
const record: BackendPostRef = {
kind: "backend-post",
version: 1,
backend: outcome.delivery.backend,
platform: outcome.delivery.platform,
accountId: outcome.delivery.accountId,
recordId: outcome.delivery.deliveryId,
};
await social.posts.deleteBackendRecord(record, { authorization });
}
Post analytics need the platform post reference from a published outcome, because it carries the PostFast record ID. Metrics include likes, comments, shares, impressions, reach, interactions, and video views where the platform reports them, plus watch time and Instagram save rate.
if (outcome.state === "published") {
const metrics = await social.analytics.getPostMetrics(outcome.post, { authorization });
}
PostFast also supports Pinterest, Telegram, and Google Business Profile. The adapter skips those accounts because they are outside Social SDK’s platform list. Comments, messages, and post feeds are not part of this adapter.
Platform options
| Platform | Social SDK option | PostFast control |
|---|---|---|
| YouTube | title, visibility, madeForKids |
youtubeTitle, youtubePrivacy, youtubeMadeForKids |
shareToFeed |
instagramPostToGrid |
|
| TikTok | disableComments, disableDuet, disableStitch |
tiktokAllowComments, tiktokAllowDuet, tiktokAllowStitch |
| TikTok | ownBrand, brandedContent, aiGenerated, draft |
tiktokBrandOrganic, tiktokBrandContent, tiktokIsAigc, tiktokIsDraft |
PostFast publishes TikTok videos with the account’s default privacy. For a TikTok video, the adapter accepts only PUBLIC_TO_EVERYONE and adds a warning; to keep a video private, set draft: true. X reply settings other than everyone have no PostFast mapping and are rejected.
Connect links
native.createConnectLink creates a hosted page where someone connects social accounts to your workspace without a PostFast account. The URL carries a signed token, so send it only to the intended person. Call it on the adapter with an operation context:
const context: AdapterOperationContext = {
backendInstance: "default",
correlationId: crypto.randomUUID(),
retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
authorization: { tenantId: "tenant-from-session" },
};
const adapter = postfast({ apiKey: process.env.POSTFAST_API_KEY! });
const { url } = await adapter.native.createConnectLink(
{ platforms: ["X", "LINKEDIN"], expiryDays: 7, externalId: "customer-42" },
context,
);
Map the new accounts to tenants in your own database before you use them. See capabilities for the generated matrix.