Workspaces

A workspace is the top-level tenant container in Plugipay. Every customer, payment, refund, invoice, subscription, API key, webhook endpoint, and team member lives inside exactly one workspace, and nothing crosses the boundary.

Most merchants will never touch this resource over the API — the workspace your key was minted in is implicit on every other call. The endpoints below exist for platform partners (Storlaunch, Fulkruma, Ripllo) provisioning merchant workspaces across many tenants, and for automation scripts that mint, rename, or archive workspaces in bulk. For the human-driven workflow (switcher, invites, transfer ownership) see Portal → Workspaces.

Workspace, account, tenant — same thing. Some API fields use accountId for historical reasons (the underlying Huudis identifier). The terms are interchangeable; we're standardizing on workspace in docs.

The workspace object

{
  "id": "ws_01HXxxxxxxxxxxxxxxxxxxxxxx",
  "accountId": "acc_01HXxxxxxxxxxxxxxxxxxxxxxx",
  "name": "Acme Production",
  "slug": "acme-prod",
  "region": "id-jakarta",
  "defaultCurrency": "IDR",
  "timezone": "Asia/Jakarta",
  "mode": "live",
  "brandName": "Acme",
  "businessEmail": "billing@acme.com",
  "createdAt": "2026-05-12T10:42:00.123Z",
  "updatedAt": "2026-05-12T10:42:00.123Z",
  "archivedAt": null
}
Field Type Notes
id string Workspace ID, prefix ws_. Stable forever.
accountId string Underlying Huudis tenant identifier. Used in X-Plugipay-On-Behalf-Of.
name string Display name. Shown in switcher, receipts, audit log. Renameable.
slug string URL-safe handle. Used in hosted checkout URLs. Renameable; old slugs 410.
region enum id-jakarta or sg-singapore. Set at creation; immutable via API.
defaultCurrency string ISO 4217. Default for new checkout sessions and plans.
timezone string IANA tz. Clock that reports and scheduled invoices run on.
mode enum test or live. Starts in test; flips after activation.
brandName string | null Public-facing brand. Used on hosted checkout when no template override.
businessEmail string | null Where Plugipay-side billing notifications go.
createdAt timestamp ISO 8601 UTC.
updatedAt timestamp ISO 8601 UTC. Touched on any settings change.
archivedAt timestamp | null When archived; null while active.

Endpoints

Retrieve a workspace

GET /v1/workspaces/{id}

Returns the workspace your key is scoped to, or — for a platform-admin key with X-Plugipay-On-Behalf-Of — the targeted merchant's workspace.

const ws = await plugipay.workspaces.get('ws_01HXxxx');
ws = plugipay.workspaces.get("ws_01HXxxx")
ws, err := client.Workspaces.Get(ctx, "ws_01HXxxx")
plugipay_curl GET '/v1/workspaces/ws_01HXxxx'

Errors: 404 not_found if the ID doesn't exist or isn't visible. 403 insufficient_scope for cross-workspace reads without the right header.

List workspaces

GET /v1/workspaces

For a merchant key, returns the single workspace it's scoped to. For a platform-admin key, returns every workspace under that partner, paginated per Pagination.

Query params: limit (1–100, default 50), cursor, mode (test/live), archived (default false).

const { data } = await plugipay.workspaces.list({ limit: 100 });
page = plugipay.workspaces.list(limit=100)
page, err := client.Workspaces.List(ctx, &plugipay.WorkspaceListParams{Limit: 100})
plugipay_curl GET '/v1/workspaces?limit=100'

Create a workspace

POST /v1/workspaces

For merchant keys, creates an additional workspace under your Huudis identity — same effect as + New workspace in the switcher. For platform-admin keys, provisions a workspace for the targeted merchant.

Body fields: name (required, slug auto-derived), brandName, businessEmail, region (id-jakarta default or sg-singapore, immutable after creation), defaultCurrency (ISO 4217, defaults to the region default).

Always send an Idempotency-Key — provisioning is expensive and a retry without one will mint a duplicate.

const ws = await plugipay.workspaces.create({
  brandName: 'Acme Staging',
  businessEmail: 'billing@acme.com',
});
ws = plugipay.workspaces.create(
    brand_name="Acme Staging",
    business_email="billing@acme.com",
)
ws, err := client.Workspaces.Create(ctx, &plugipay.WorkspaceCreateParams{
    BrandName:     plugipay.String("Acme Staging"),
    BusinessEmail: plugipay.String("billing@acme.com"),
})
plugipay_curl POST '/v1/workspaces' \
  '{"brandName":"Acme Staging","businessEmail":"billing@acme.com"}'

Errors: 403 insufficient_scope if your key can't provision (platform-admin scope required for partner provisioning). 422 region_immutable if you tried to change region in a follow-up patch.

Update workspace settings

PATCH /v1/workspaces/{id}

Partial update; send only the fields you want to change. Patchable: name, slug, brandName, businessEmail, defaultCurrency, timezone. Not patchable: region, mode (use the activation flow), id, accountId, createdAt.

await plugipay.workspaces.update('ws_01HXxxx', {
  brandName: 'Acme (renamed)',
  defaultCurrency: 'USD',
});
plugipay.workspaces.update(
    "ws_01HXxxx",
    brand_name="Acme (renamed)",
    default_currency="USD",
)
_, err := client.Workspaces.Update(ctx, "ws_01HXxxx", &plugipay.WorkspaceUpdateParams{
    BrandName:       plugipay.String("Acme (renamed)"),
    DefaultCurrency: plugipay.String("USD"),
})
plugipay_curl PATCH '/v1/workspaces/ws_01HXxxx' \
  '{"brandName":"Acme (renamed)","defaultCurrency":"USD"}'

Slug renames break old links. Hosted checkout URLs and deep-links embed the slug. We do not keep slug redirects — the old slug returns 410 Gone immediately after a rename. Treat it like a domain migration.

Archive a workspace

DELETE /v1/workspaces/{id}

Archives the workspace. Owner role only. Live mode disables immediately, subscriptions cancel at period end, webhooks stop after a 24-hour grace, API keys revoke. Data retained for 90 days then permanently purged; restore from the portal within that window.

await plugipay.workspaces.delete('ws_01HXxxx');
plugipay.workspaces.delete("ws_01HXxxx")
err := client.Workspaces.Delete(ctx, "ws_01HXxxx")
plugipay_curl DELETE '/v1/workspaces/ws_01HXxxx'

Returns 204. Errors: 403 owner_required, 409 has_open_balance if the ledger isn't settled (run a payout first).

Members

A workspace has a team. Each member has exactly one role.

Role Scope
owner Everything, including ownership transfer, archive, billing. Exactly one per workspace.
admin Everything except Owner-only actions. Multiple allowed.
member Day-to-day operations: payments, refunds, webhooks, customers. No team or API-key admin.
read_only View everything, change nothing. For accountants, auditors, support.

Roles are workspace-scoped — the same Huudis identity can be owner in one and read_only in another.

The member object

{
  "id": "mem_01HYxxxxxxxxxxxxxxxxxxxxxx",
  "email": "alice@acme.com",
  "role": "admin",
  "joinedAt": "2026-05-12T10:42:00.123Z"
}

List members

GET /v1/workspaces/{id}/members
const members = await plugipay.workspaces.listMembers('ws_01HXxxx');
members = plugipay.workspaces.list_members("ws_01HXxxx")
plugipay_curl GET '/v1/workspaces/ws_01HXxxx/members'

Invite a member

POST /v1/workspaces/{id}/members

Body: { "email": "...", "role": "admin" | "member" | "read_only" }. Plugipay emails the invite; recipient accepts via Huudis SSO. Invitations expire after 7 days. Can't invite at owner — use Transfer ownership in the portal.

Update a member's role

PATCH /v1/workspaces/{id}/members/{memberId}

Body: { "role": "admin" | "member" | "read_only" }. Owners and Admins can promote/demote anyone below their own role.

Remove a member

DELETE /v1/workspaces/{id}/members/{memberId}

Returns 204. Can't remove the Owner — transfer first, or you'll get 409 owner_immutable.

Acting across workspaces: X-Plugipay-On-Behalf-Of

Platform-admin keys (Storlaunch, Fulkruma, Ripllo, other partners with a signed agreement) can address any merchant workspace under their umbrella with one header:

X-Plugipay-On-Behalf-Of: acc_01HXxxxxxxxxxxxxxxxxxxxxxx

The same HMAC signature applies — the header just scopes the call. Without it, a platform-admin key falls back to the partner's own internal workspace, which is rarely what you want. Merchant keys silently ignore the header; cross-tenant access requires explicit partner provisioning. See Conventions → X-Plugipay-On-Behalf-Of for the full security model.

SDK shortcut. Node exposes client.forMerchant(accountId) — returns a cloned client with the header pre-set. Handy when iterating over many merchants.

Events

Workspace lifecycle and membership changes fire webhook events. Subscribe per Webhooks.

Event When
workspace.created New workspace provisioned (by merchant or partner).
workspace.updated Any settings patch (name, slug, brand, currency, timezone).
workspace.archived Owner archived the workspace.
workspace.restored Archived workspace restored within the 90-day window.
workspace.member_invited Invitation sent. Doesn't fire on accept — that's member_joined.
workspace.member_joined Recipient accepted via Huudis SSO.
workspace.member_removed Member removed by Owner or Admin.
workspace.member_role_changed Role promoted or demoted. previousAttributes.role populated.
workspace.ownership_transferred Owner handed off. Payload includes both identities.

For platform-admin keys, every workspace-scoped event also fires through your partner-level webhook endpoint — useful for centralized provisioning observability. The merchant workspaceId is always on the envelope.

Next

  • API keys — how per-workspace key scoping works.
  • Webhooks — full event catalog and signature scheme.
  • Portal → Workspaces — the same concepts from the dashboard side, including ownership transfer and the danger-zone flows.
Plugipay — Payments that don't tax your success