Customers

A customer is the persistent record of someone who can pay you — Plugipay's address book under every payment, subscription, and invoice. Guest checkouts work without one; the moment you want a second charge, a stored token, or a clean receipt history under a single identity, you create a customer. The Python SDK wraps the four customer endpoints behind plugipay.customers. Each call returns a Customer instance (a dict-backed dataclass) or a PageResult[Customer] for the list endpoint. For HTTP shapes, query parameters, and the full field reference, see API → Customers.

Namespace

plug.customers      # _Customers — every customer method below

The namespace is instantiated on the PlugipayClient constructor and shares its underlying httpx.Client. There is no per-namespace state — calls are independent.

Methods

create

plug.customers.create(
    *,
    email: str | None = None,
    name: str | None = None,
    phone: str | None = None,
    external_id: str | None = None,
    metadata: dict[str, str] | None = None,
) -> Customer

Creates a customer in the workspace this key belongs to (or the one named by on_behalf_of, for platform keys). Every field is optional — a bare create() returns a customer with only id and timestamps; you fill it in later via update.

customer = plug.customers.create(
    email="ada@example.com",
    name="Ada Lovelace",
    external_id="user_19823",
    metadata={"segment": "enterprise"},
)
print(customer["id"])     # "cus_01HX..."
print(customer.raw)       # full server payload

The SDK auto-generates an idempotency key (idem_<uuid4>) per call, so retries against transient network errors are safe. If you want to control retries yourself — say, to dedupe by your own request id — use the lower-level plug.request(...) and pass idempotency_key=.

get

plug.customers.get(customer_id: str) -> Customer

Returns the customer with id exactly equal to customer_id. The id must carry the cus_ prefix. Raises PlugipayError with status=404, code="not_found" for both missing and cross-workspace ids — Plugipay returns 404 (not 403) to avoid leaking existence.

from plugipay import PlugipayError

try:
    customer = plug.customers.get("cus_01HXAB7K3M9N2P5QRS8TVWXY3Z")
except PlugipayError as err:
    if err.status == 404:
        customer = plug.customers.create(email="ada@example.com")
    else:
        raise

list

plug.customers.list(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    email: str | None = None,
) -> PageResult[Customer]

Returns a single page of customers. Default page size on the server is 20, clamped to [1, 100]. The email filter is an exact, case-sensitive match — useful for a "does this exist?" check before creating, but external_id is the dedupe key Plugipay actually enforces.

Plugipay uses cursor pagination; PageResult.has_more tells you when to stop and PageResult.cursor is the next page's cursor. See Pagination for the cursor protocol — don't reinvent it.

cursor = None
while True:
    page = plug.customers.list(limit=100, cursor=cursor)
    for c in page.data:
        print(c.get("email"))
    if not page.has_more:
        break
    cursor = page.cursor

update

plug.customers.update(
    customer_id: str,
    *,
    email: str | None = None,
    name: str | None = None,
    phone: str | None = None,
) -> Customer

Partial update. Pass only the fields you want to change. Omitted fields are not modified. The Python SDK currently exposes email, name, and phone as kwargs; to update external_id, tax_id, default_payment_token_id, or metadata (which the HTTP API also accepts), drop down to plug.request("PATCH", "/api/v1/customers/<id>", body={...}).

updated = plug.customers.update(
    "cus_01HXAB7K3M9N2P5QRS8TVWXY3Z",
    name="Ada Lovelace (Acme Corp)",
)

No delete method. Plugipay deliberately does not expose customer deletion via the API — a hard delete would cascade through payments, invoices, refunds, and ledger entries. To archive a customer, PATCH metadata.archived = "true" via plug.request and filter client-side. For GDPR erasure, clear personal fields then email support@plugipay.com.

Types

from plugipay import Customer, PageResult

Customer is a Resource subclass — a dataclass wrapping raw: dict[str, Any]. It does not enforce a schema; whatever the server returns lives in raw, and you read it via customer["field"], customer.get("field"), or customer.raw. See API → Customers → The customer object for every field name and type.

PageResult[Customer] is the only non-Resource dataclass you'll touch here:

@dataclass
class PageResult(Generic[T]):
    data: list[T]
    cursor: str | None = None
    has_more: bool = False

Common patterns

Dedupe via external_id before create. email is not unique in Plugipay (some businesses serve multiple legal entities through one inbox), so don't use it as a key. external_id is enforced unique per workspace. The check-then-create flow:

page = plug.customers.list(limit=1, email="ada@example.com")
if page.data:
    customer = page.data[0]
else:
    customer = plug.customers.create(
        email="ada@example.com",
        external_id="user_19823",
    )

Attribute vs dict access. Resources implement __getitem__ and .get(...), not __getattr__. Read fields as dict keys:

customer["id"]              # raises KeyError if missing
customer.get("email")       # returns None if missing
customer.get("email", "")   # returns "" if missing
customer.raw                # full dict, including unmapped fields

Context manager for short scripts. The client owns an httpx.Client by default. Use with to release sockets:

with PlugipayClient(key_id="ak_live_…", secret="…") as plug:
    customer = plug.customers.create(email="ada@example.com")
# httpx pool closed here

Branch on external_id collisions. A duplicate external_id returns 409 CONFLICT:

from plugipay import PlugipayError

try:
    plug.customers.create(email="ada@example.com", external_id="user_19823")
except PlugipayError as err:
    if err.status == 409 and err.code == "conflict":
        # Already exists — fetch by external_id via list filter or your local cache.
        ...
    else:
        raise

Errors

Customer endpoints raise PlugipayError (the base) — the SDK does not define CustomerNotFound or similar subclasses. Branch on err.status and err.code:

err.status err.code Cause
400 validation_error Field shape wrong, unknown field, email not RFC-valid.
404 not_found Customer doesn't exist or lives in another workspace.
409 conflict external_id already used in this workspace.
409 idempotency_key_reused Same Idempotency-Key against a different body.
403 insufficient_scope API key lacks plugipay:customer:create / :read / :write.

Transport failures surface as PlugipayNetworkError (status=0, code="network_error") or PlugipayTimeoutError (status=0, code="timeout"). Both are subclasses of PlugipayError, so a single except PlugipayError catches all of the above. See Errors for the full hierarchy.

Next

Plugipay — Payments that don't tax your success