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
accountIdfor 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 Goneimmediately 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.