@duabalabs/connect-client
Typed client for DuabaConnect — the engine that powers two client-facing capabilities other apps embed on behalf of their own clients:
social— connect a client’s social platforms, then run product campaigns that auto-generate image/video posts and publish them on a schedule (Postiz under the hood).automations— a catalog of parameterized AI/n8n workflows a client can deploy and run against their commerce journey.
It calls the API-key-gated REST surface at /api/connect/*. Source: apps/duabaconnect/packages/connect-client/src/index.ts.
Install
npm install @duabalabs/connect-client
# or
pnpm add @duabalabs/connect-clientcreateConnectClient(options)
import { createConnectClient } from "@duabalabs/connect-client";
const connect = createConnectClient({
baseUrl: "https://api.duabaconnect.com", // optional, this is the default
apiKey: process.env.CONNECT_API_KEY, // server-only
});| Option | Type | Default | Notes |
|---|---|---|---|
baseUrl | string | https://api.duabaconnect.com | Connect API base URL |
apiKey | string | — | Connect API key; sent as X-Connect-Api-Key. Server-only |
fetch | typeof fetch | global fetch | Custom fetch for non-browser / edge runtimes |
The returned client has two namespaces: connect.social and connect.automations.
Authentication & clientRef
The API key identifies your app (the integrator), not an end user. Mint it once with the connect_issueApiKey cloud function and store it in CONNECT_API_KEY. Every method carries a clientRef — your stable identifier for the end client (e.g. a Sellub seller’s account id or email). Connect maps it to its own account/Postiz customer. Keep clientRef aligned with the metadata.email you set on Sellub checkout so entitlements line up.
# env
CONNECT_API_KEY=ck_live_…The key is server-only. If you have no global fetch (older Node), pass options.fetch; otherwise the factory throws "[connect-client] No fetch implementation found.".
Errors
Every method resolves to an object with success: boolean. On a non-2xx response or success === false, the result carries a human-readable error — check it before using the data. The package also exports a ConnectApiError class ({ message, status }).
const r = await connect.social.createCampaign({ /* … */ });
if (!r.success) {
console.error("Connect error:", r.error);
return;
}connect.social
| Method | Signature | Purpose |
|---|---|---|
connectPlatform | (input: ConnectPlatformRequest) => Promise<ConnectPlatformResponse> | Start connecting a client’s platform; returns an authorizationUrl to redirect them to |
listPlatforms | (clientRef: string) => Promise<ConnectedPlatformsResponse> | The client’s connected platforms |
createCampaign | (input: CampaignCreateRequest) => Promise<CampaignResponse> | Create a product campaign — Connect generates + schedules + publishes posts |
listCampaigns | (clientRef: string) => Promise<CampaignListResponse> | The client’s campaigns |
getCampaign | (campaignId: string) => Promise<CampaignResponse> | One campaign |
setCampaignStatus | (campaignId: string, status: "active" | "paused") => Promise<CampaignResponse> | Pause/resume |
generateContent | (input: GenerateContentRequest) => Promise<GenerateContentResponse> | Generate one post’s content (preview / “AI suggest”), without scheduling |
listPosts | (campaignId: string) => Promise<PostListResponse> | Posts generated/scheduled/published for a campaign |
publishPost | (postId: string, options?: { publishDate?: string }) => Promise<PublishPostResponse> | Publish a ready post (“resend for posting”); fails if media is still generating |
schedulePost | (input: SchedulePostInput) => Promise<SocialPostResponse> | Schedule a one-off / recurring post (auto-publishes at scheduledAt) |
listCalendar | (input: { clientRef: string; from?: string; to?: string }) => Promise<CalendarResponse> | Posts in a date window (calendar feed) |
updatePost | (postId: string, patch: PostPatch) => Promise<SocialPostResponse> | Edit a post (caption / reschedule / status / media) |
deletePost | (postId: string, clientRef: string) => Promise<DeleteResponse> | Delete a post |
Connect a platform
const res = await connect.social.connectPlatform({
clientRef: "seller_123",
platform: "instagram",
returnUrl: "https://app.sellub.com/seller/social",
});
if (res.success && res.authorizationUrl) {
redirect(res.authorizationUrl); // client authorizes the platform
}Run a product campaign
const res = await connect.social.createCampaign({
clientRef: "seller_123",
product: {
id: "prod_42",
name: "Shea Butter 200g",
description: "Raw, unrefined shea butter.",
imageUrl: "https://cdn.example.com/shea.jpg",
price: 4500, // integrator's own units
currency: "GHS",
url: "https://acme.sellub.com/p/shea-200g",
},
platforms: ["instagram", "facebook", "tiktok"],
mediaType: "video",
frequencyPerWeek: 3,
brief: "Warm, earthy tone; emphasise it's raw and locally sourced.",
});
const campaign = res.campaign; // { id, status, platforms, nextPostAt, … }Connect turns this into review-ready posts on a cadence (7 days / frequencyPerWeek). Posts are not auto-published — surface listPosts(campaignId) and let the client publishPost(postId) the ready ones.
One-off content preview
const res = await connect.social.generateContent({
clientRef: "seller_123",
product,
mediaType: "image",
platforms: ["instagram"],
brief: "Punchy, single-line caption.",
});
// res.content: { caption, hashtags, mediaUrl?, status: "ready" | "generating", jobId? }Calendar
await connect.social.schedulePost({
clientRef: "seller_123",
platform: "x",
caption: "New drop is live 🎉",
mediaUrls: ["https://cdn.example.com/drop.jpg"],
scheduledAt: "2026-07-01T09:00:00Z",
timezone: "Africa/Accra",
recurrence: null, // or "daily" | "weekly" | "monthly"
});
const cal = await connect.social.listCalendar({
clientRef: "seller_123",
from: "2026-07-01",
to: "2026-07-31",
});connect.automations
| Method | Signature | Purpose |
|---|---|---|
listCatalog | () => Promise<AutomationCatalogResponse> | Available automations (with Sellub plan codes) |
getEntitlement | (input: { clientRef: string; workflowKey: string }) => Promise<EntitlementResponse> | Is the client entitled (paid + unexpired)? |
deploy | (input: DeployAutomationRequest) => Promise<DeployAutomationResponse> | Provision a parameterized automation for a client (after billing) |
listInstances | (clientRef: string) => Promise<InstanceListResponse> | The client’s deployed automation instances |
run | (input: RunAutomationRequest) => Promise<RunAutomationResponse> | Trigger a run of a deployed instance |
getOutput | (input: { clientRef: string; runId: string }) => Promise<AutomationOutputResponse> | Fetch a run’s output/status |
Deploy & run a workflow
// 1. Show the catalog
const cat = await connect.automations.listCatalog();
// cat.automations: AutomationCatalogItem[] — workflowKey, pricingModel, priceGhs,
// sellubPlanCode, parameters[] …
// 2. After billing through Sellub, check entitlement
const ent = await connect.automations.getEntitlement({
clientRef: "seller_123",
workflowKey: "viral-trend-detection",
});
if (!ent.active) return; // send them to checkout first
// 3. Deploy with the client's parameter values
const dep = await connect.automations.deploy({
clientRef: "seller_123",
workflowKey: "viral-trend-detection",
config: { NICHE: "skincare", REGION: "Ghana", KEYWORDS: "shea, natural" },
});
const instanceId = dep.instance?.id;
// 4. Run it
const run = await connect.automations.run({
clientRef: "seller_123",
instanceId: instanceId!,
input: {}, // optional, merged over deploy-time config
});
// 5. Read the output
const out = await connect.automations.getOutput({
clientRef: "seller_123",
runId: run.runId!,
});
// out.result: { runId, status: "running" | "succeeded" | "failed", output?, finishedAt? }Exported types
The package exports its full type surface. Key ones:
| Type | What it is |
|---|---|
SocialPlatform | "instagram" | "facebook" | "tiktok" | "x" | "linkedin" | "youtube" | "pinterest" | "threads" |
MediaType | "image" | "video" |
Recurrence | "daily" | "weekly" | "monthly" |
CampaignStatus | "draft" | "active" | "paused" | "completed" |
PostStatus | "generating" | "ready" | "scheduled" | "publishing" | "published" | "failed" |
AutomationPricingModel | "subscription" | "oneoff" |
ProductRef | { id, name, description?, imageUrl?, price?, currency?, url? } |
Campaign, SocialPost, GeneratedContent | Social entities |
AutomationCatalogItem, AutomationParameter, AutomationInstance, AutomationOutput | Automation entities |
ConnectClientOptions, ConnectClient, ConnectApiError | Client + error |
Plus the request/response interfaces for every method (e.g. CampaignCreateRequest, DeployAutomationRequest, EntitlementResponse).
How Sellub embeds Connect for its clients
A worked end-to-end shape — Sellub offering a seller “promote this product” and “deploy this automation”:
// Sellub server — one Connect client, keyed per seller via clientRef
import { createConnectClient } from "@duabalabs/connect-client";
const connect = createConnectClient({ apiKey: process.env.CONNECT_API_KEY });
const clientRef = seller.email; // aligns with Sellub checkout metadata.email
// "Promote this product" → a Connect campaign
await connect.social.createCampaign({
clientRef,
product: toProductRef(sellubProduct), // Sellub's catalog → ProductRef
platforms: ["instagram", "facebook"],
mediaType: "image",
frequencyPerWeek: 2,
});
// "Add this automation" → bill via Sellub, then deploy via Connect
const ent = await connect.automations.getEntitlement({ clientRef, workflowKey });
if (ent.active) {
await connect.automations.deploy({ clientRef, workflowKey, config });
}The two engines meet at money and identity: Sellub runs checkout (Paystack, GHS) and emits the payment.succeeded webhook that becomes a Connect Order; Connect reads that entitlement (getEntitlement) before provisioning. See the Sellub sellub-client reference.