Workspaces

A workspace is a Plugipay tenant — one workspace, one set of customers, one ledger, one set of payment adapters. Most accounts use exactly one workspace; multi-brand operators (running, say, two retail brands out of one company) split them. This page covers the merchant-facing plugipay.workspaces namespace, which lets you list, create, update, and delete the workspaces the calling key can access. For the underlying HTTP surface see API: Workspaces; for partner-provisioned workspaces (managed by Storlaunch / Fulkruma / Ripllo on your behalf) see the admin.provisionWorkspace method in Reference.

Namespace

plugipay.workspaces — every method on this namespace:

plugipay.workspaces.list()
plugipay.workspaces.create(input)
plugipay.workspaces.update(id, patch)
plugipay.workspaces.delete(id)

There's no get — the list response is small (workspaces are bounded by your account, not by traffic) and contains every field, so a separate retrieve isn't needed. To pick one, list and filter client-side.

Methods

workspaces.list

Signature. plugipay.workspaces.list(): Promise<Workspace[]>

Returns every workspace the current key can access. For a regular merchant key, that's usually the one workspace the key belongs to; for a platform-admin key, it's every workspace under that admin's purview.

import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
});

const workspaces = await plugipay.workspaces.list();
for (const w of workspaces) {
  console.log(`${w.id}: ${w.brandName ?? '(no brand)'}  →  ${w.businessEmail ?? '(no email)'}`);
}

workspaces.create

Signature. plugipay.workspaces.create(input): Promise<Workspace>

Creates a new workspace under your account. Both brandName and businessEmail are optional — you can leave them empty at create and fill them in via update later (or via the dashboard's onboarding wizard). The SDK auto-attaches an Idempotency-Key.

const ws = await plugipay.workspaces.create({
  brandName: 'Acme Retail',
  businessEmail: 'billing@acme-retail.id',
});

console.log(ws.id, ws.accountId);

The new workspace starts empty — no adapters configured, no API keys minted, no customers. Each of those is set up through its own namespace.

workspaces.update

Signature. plugipay.workspaces.update(id, patch): Promise<Workspace>

PATCH semantics — only the fields you pass are touched.

const ws = await plugipay.workspaces.update('ws_01HX...', {
  brandName: 'Acme Retail Indonesia',
  businessEmail: 'finance@acme.id',
});

workspaces.delete

Signature. plugipay.workspaces.delete(id): Promise<void>

Deletes a workspace. Plugipay refuses deletion if the workspace has any outstanding obligations — non-zero balance, in-flight payouts, unfinalized invoices — in which case the call returns state_conflict. Resolve those first (e.g. sweep the balance, void open invoices), then retry.

await plugipay.workspaces.delete('ws_01HX...');

Deletion is final. All workspace data — customers, payment history, ledger entries, receipts — is retained for a regulatory window and then purged. There's no un-delete. For "I just want to stop using this workspace", consider revoking the API keys instead.

Types

interface Workspace {
  id: string;                  // 'ws_...'
  accountId: string;           // 'acc_...' — your account
  brandName: string | null;
  businessEmail: string | null;
  createdAt: string;
  updatedAt: string;
}

For the full reference (including the relationship to accountId, partner workspace overlays, and the deletion holdback rules), see API: Workspaces.

Common patterns

Pick the workspace for a multi-brand operator

If you operate multiple brands out of one Plugipay account, your code needs to select the right workspace at startup:

async function pickWorkspace(brandName: string): Promise<Workspace> {
  const all = await plugipay.workspaces.list();
  const found = all.find((w) => w.brandName === brandName);
  if (!found) throw new Error(`No workspace for brand ${brandName}`);
  return found;
}

In practice you'd cache the workspace ID at deploy time rather than looking it up on every request — the ID is stable.

Provision a new brand

The standard "we're launching a new brand" flow:

async function launchBrand(brand: { name: string; email: string }) {
  // 1. Create the workspace.
  const ws = await plugipay.workspaces.create({
    brandName: brand.name,
    businessEmail: brand.email,
  });

  // 2. Mint an API key for the new workspace.
  //    NOTE: you need to act under the new workspace for this — see "Act on
  //    behalf of a workspace" below.
  const scoped = plugipay.forMerchant(ws.accountId);
  const key = await scoped.apiKeys.create({
    description: `${brand.name} production key`,
  });

  // 3. Configure adapters (if you have direct provider credentials).
  await scoped.adapters.updateXendit({ /* … */ });

  // 4. Register a webhook endpoint.
  await scoped.webhookEndpoints.create({
    url: `https://yourapp.com/webhooks/plugipay/${ws.id}`,
  });

  return { workspaceId: ws.id, keyId: key.keyId, secret: key.secret! };
}

Audit workspaces

For an internal "what brands do we run?" report:

async function brandAudit() {
  const all = await plugipay.workspaces.list();
  return all.map((w) => ({
    id: w.id,
    brand: w.brandName ?? '(unnamed)',
    email: w.businessEmail ?? '(no contact)',
    ageDays: Math.floor((Date.now() - Date.parse(w.createdAt)) / 86_400_000),
  }));
}

Act on behalf of a workspace (platform admin)

If you're a platform admin with plugipay:platform:admin scope (Storlaunch, Fulkruma, Ripllo), call forMerchant to scope subsequent calls to a specific workspace's accountId:

const platform = new PlugipayClient({
  keyId: process.env.PLUGIPAY_PLATFORM_KEY_ID!,
  secret: process.env.PLUGIPAY_PLATFORM_SECRET!,
});

const merchant = platform.forMerchant('acc_01HX...');
await merchant.checkoutSessions.create({ /* runs as the merchant */ });

See Authentication for the underlying X-Plugipay-On-Behalf-Of mechanism.

Safely delete a workspace

Wrap the deletion in a small dry-run guard so you don't surprise yourself:

async function deleteWorkspaceSafely(workspaceId: string) {
  // Switch to the workspace context to inspect it.
  const scoped = plugipay.forMerchant(workspaceId.replace(/^ws_/, 'acc_'));
  // (For your own workspaces, the IDs are linked via Workspace.accountId.)

  const balance = await scoped.payouts.balance();
  if (balance.available > 0) {
    throw new Error(`Workspace has non-zero balance: ${balance.available} ${balance.currency}`);
  }
  const openInvoices = await scoped.invoices.list({ status: 'open', limit: 1 });
  if (openInvoices.data.length > 0) {
    throw new Error('Workspace has open invoices; void or pay them first');
  }
  await plugipay.workspaces.delete(workspaceId);
}

Errors

Code Status Cause
validation_error 400 Bad businessEmail format, brandName too long.
not_found 404 Workspace ID doesn't exist or doesn't belong to your account.
state_conflict 409 Deletion blocked by non-zero balance, in-flight payouts, or open invoices.
forbidden 403 Key lacks plugipay:workspace:write scope.

Next

Plugipay — Payments that don't tax your success