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
deletemethod. 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, PATCHmetadata.archived = "true"viaplug.requestand 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
- API → Customers — field-level reference for every property on the customer object.
- Checkout sessions — how a customer pays the first time.
- Subscriptions — recurring billing tied to a customer.
- Errors — exception hierarchy and request-id correlation.