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'

externalId is unique per workspace. If you pass an externalId that already exists, the API returns 409 and the SDK throws PlugipayError with code: '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

Plugipay — Payments that don't tax your success