ConnectSDKsconnect-client

@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-client

createConnectClient(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
});
OptionTypeDefaultNotes
baseUrlstringhttps://api.duabaconnect.comConnect API base URL
apiKeystringConnect API key; sent as X-Connect-Api-Key. Server-only
fetchtypeof fetchglobal fetchCustom 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

MethodSignaturePurpose
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

MethodSignaturePurpose
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:

TypeWhat 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, GeneratedContentSocial entities
AutomationCatalogItem, AutomationParameter, AutomationInstance, AutomationOutputAutomation entities
ConnectClientOptions, ConnectClient, ConnectApiErrorClient + 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.