Customers
A customer is the persistent record of someone who can pay you — the address book that sits under every charge, subscription, and invoice in your workspace. This page covers the plugipay.customers namespace on the Node SDK: the four methods, their typed inputs, and the patterns most teams reach for. For the underlying HTTP surface and the full field tables, see API: Customers; for the data model, see Concepts → Customer.
Namespace
plugipay.customers — every method on this namespace:
plugipay.customers.create(input)
plugipay.customers.get(id)
plugipay.customers.list(params?)
plugipay.customers.update(id, patch)
There's no delete — customer deletion is intentionally not exposed via the API. See the API page for the reasoning. To "remove" a customer, set metadata.archived = "true" on them and filter that out in your dashboards.
Methods
customers.create
Signature. plugipay.customers.create(input): Promise<Customer>
Creates a customer in the workspace the API key belongs to. Every field is optional — a bare {} body returns a customer with just an id and timestamps, which you can fill in later via update. The SDK auto-generates an Idempotency-Key for you, so retrying a transient network failure won't create duplicates.
import { PlugipayClient } from '@forjio/plugipay-node';
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
const customer = await plugipay.customers.create({
email: 'alice@example.com',
name: 'Alice Tan',
phone: '+62812xxxxxxxx',
externalId: 'user_19823',
metadata: { plan: 'pro', signup_source: 'web' },
});
console.log(customer.id); // → 'cus_01HXAB7K3M9N2P5QRS8TVWXY3Z'
externalIdis unique per workspace. If you pass anexternalIdthat already exists, the API returns409and the SDK throwsPlugipayErrorwithcode: 'conflict'. The Look up by external ID pattern below handles this cleanly.
customers.get
Signature. plugipay.customers.get(id): Promise<Customer>
Fetches one customer by its cus_* ID. Throws PlugipayError with code: 'not_found' (status 404) if the customer doesn't exist or is in a different workspace.
const customer = await plugipay.customers.get('cus_01HXAB7K3M9N2P5QRS8TVWXY3Z');
console.log(customer.email);
customers.list
Signature. plugipay.customers.list(params?): Promise<{ data: Customer[]; cursor: string | null; hasMore: boolean }>
Cursor-paginated list. Filters: email (exact match), limit (1–100, default 25), cursor. See Pagination for the full cursor model — it's identical across every list method on the SDK.
// First page
const page = await plugipay.customers.list({ limit: 50 });
for (const c of page.data) {
console.log(c.id, c.email);
}
// Next page
if (page.hasMore) {
const next = await plugipay.customers.list({ limit: 50, cursor: page.cursor! });
}
Filtering by email:
const { data } = await plugipay.customers.list({ email: 'alice@example.com' });
Email is not enforced unique server-side — you may legitimately have two customers sharing one address (a personal account + a business account for the same person). Treat the response as a list, not a single value.
customers.update
Signature. plugipay.customers.update(id, patch): Promise<Customer>
PATCH semantics — only the fields you pass are touched. Pass null to clear a nullable field (e.g. clear a phone number you no longer want on receipts).
const updated = await plugipay.customers.update('cus_01HX...', {
name: 'Alice T. Wijaya',
phone: '+62811xxxxxxxx',
});
The SDK does not expose metadata on the update method because the underlying API treats metadata as a separate concern with stricter validation. To touch metadata, use client.request() directly — or, more commonly, re-create the customer if it's net-new.
Types
The shape returned by every method:
interface Customer {
id: string; // 'cus_...'
accountId: string; // workspace
email: string | null;
name: string | null;
phone: string | null;
externalId: string | null;
createdAt: string; // ISO-8601
updatedAt: string;
}
For the full field reference (including server-only fields like arn and validation rules on each input field), see API: Customers → Fields.
Common patterns
Look up by external ID (or create)
Your CRM is the source of truth; Plugipay's externalId is a foreign-key into it. The robust "find or create" pattern uses the 409 conflict path:
async function ensureCustomer(externalId: string, attrs: { email?: string; name?: string }) {
try {
return await plugipay.customers.create({ externalId, ...attrs });
} catch (err) {
if (err instanceof PlugipayError && err.code === 'conflict') {
// Already exists — find by externalId via list filter
const { data } = await plugipay.customers.list({ limit: 1 });
// (no externalId filter in v1; in practice you cache the cus_ ID on your side)
throw new Error('Cache miss — store the cus_ id on creation');
}
throw err;
}
}
In practice you don't want to round-trip on every charge. Store the resulting cus_* ID on your side when you first create the customer, keyed by your own user ID.
Look up by email
async function customerByEmail(email: string) {
const { data } = await plugipay.customers.list({ email });
return data[0] ?? null;
}
Because email isn't unique, treat this as best-effort.
Walk the whole customer book
async function* allCustomers() {
let cursor: string | undefined;
while (true) {
const page = await plugipay.customers.list({ limit: 100, cursor });
for (const c of page.data) yield c;
if (!page.hasMore) break;
cursor = page.cursor!;
}
}
for await (const c of allCustomers()) {
// export to your warehouse, run reconciliation, etc.
}
Tag customers for reconciliation
Metadata is the right place to stash your-side reconciliation keys (CRM ID, accounting ID, sales rep). It's free-form string–to–string and shows up on every event involving the customer.
await plugipay.customers.create({
email: 'finance@bigcorp.id',
name: 'BigCorp Indonesia',
externalId: 'crm_88012',
metadata: {
accounting_ref: 'AR-2026-0118',
sales_rep: 'budi',
tier: 'enterprise',
},
});
Errors
The errors you'll see most often on this namespace:
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Bad email format, name too long, unknown field. |
conflict |
409 | Duplicate externalId in this workspace. |
not_found |
404 | Customer ID doesn't exist or lives in another workspace. |
forbidden |
403 | Key lacks plugipay:customer:create (or read/update) scope. |
Every one of these comes through as a PlugipayError instance — see Errors for the full handling guide and API errors for the complete catalog.
Next
- Checkout sessions — charging a customer.
- Subscriptions — recurring billing on a customer.
- Portal sessions — let the customer self-serve.
- API: Customers — HTTP-level reference.