Authentication
Authenticate by passing a key pair to the PlugipayClient constructor. The SDK signs every outbound request with HMAC-SHA256 — you never touch the signing recipe.
from plugipay import PlugipayClient
plug = PlugipayClient(
key_id="pk_live_xxxxxxxxxxxxxxxx",
secret="sk_live_xxxxxxxxxxxxxxxxxxxxxxxx",
)
This page covers every constructor option, the conventional way to pass credentials in production, and the two situations where you'd want to override defaults (platform keys, custom HTTP client).
For the HMAC recipe itself — what the SDK is doing under the hood — see API authentication.
The constructor
PlugipayClient(
*,
key_id: str,
secret: str,
base_url: str | None = None,
on_behalf_of: str | None = None,
timeout: float = 30.0,
http: httpx.Client | None = None,
)
All arguments are keyword-only (note the leading *). key_id and secret are required and must be non-empty — the constructor raises ValueError otherwise.
| Argument | Default | When to set it |
|---|---|---|
key_id |
required | Your pk_test_* or pk_live_* access key id. |
secret |
required | The matching sk_test_* / sk_live_* secret. |
base_url |
https://plugipay.com |
Self-hosted / staging Plugipay. Trailing slashes are stripped. |
on_behalf_of |
None |
Platform keys acting on a specific merchant. Sets X-Plugipay-On-Behalf-Of on every request. |
timeout |
30.0 seconds |
Per-request timeout. See Timeouts. |
http |
new httpx.Client |
Pass your own pre-configured httpx.Client. See Custom httpx client. |
Loading credentials
The recommended pattern is environment variables — never hardcode secrets:
import os
from plugipay import PlugipayClient
plug = PlugipayClient(
key_id=os.environ["PLUGIPAY_KEY_ID"],
secret=os.environ["PLUGIPAY_KEY_SECRET"],
)
The SDK doesn't read env vars on its own — that's deliberate, so it stays explicit about which keys are in use. But every example on these pages uses PLUGIPAY_KEY_ID / PLUGIPAY_KEY_SECRET as the conventional names. Match them in your .env, docker-compose.yml, or kubectl secret manifest.
The secret is shown once. When you mint a key in the portal, Plugipay displays the secret in a dialog. If you lose it, you have to revoke and re-mint. There's no recovery flow. Store it in a secret manager (Doppler, Vault, AWS Secrets Manager, GCP Secret Manager) or your platform's env-var system — never commit it to git.
Test vs live mode
The _test_ / _live_ infix in the key id encodes the environment:
pk_test_*+sk_test_*→ test mode (sandbox; safe to spam; no real money).pk_live_*+sk_live_*→ live mode (real charges).
Both share the same base_url (https://plugipay.com); the key determines the mode. Cross-mode calls return 401 mode_mismatch.
For most projects, set PLUGIPAY_KEY_ID / PLUGIPAY_KEY_SECRET separately in dev/staging/prod environments and let your deployment system swap them in.
Platform keys (on_behalf_of)
If you're a Plugipay partner with a platform-admin key, you call the API on behalf of merchant workspaces by setting X-Plugipay-On-Behalf-Of. The SDK gives you two ways to do that.
Per-client (set once)
plat = PlugipayClient(
key_id="pk_live_platform_xxx",
secret="sk_live_platform_xxx",
on_behalf_of="acc_01HXXXXXXXXXXXXXXXXXXXXXXX",
)
invoices = plat.invoices.list() # scoped to acc_01HX… automatically
Per-call: for_merchant()
The cleaner pattern when you're iterating across multiple merchants is to create one platform client and derive scoped sub-clients:
plat = PlugipayClient(key_id="pk_live_platform_xxx", secret="…")
for merchant_id in ("acc_01H…", "acc_02H…", "acc_03H…"):
scoped = plat.for_merchant(merchant_id)
page = scoped.payouts.balance()
print(merchant_id, page["available"])
for_merchant() returns a brand-new PlugipayClient with on_behalf_of baked in. It does not share the underlying connection pool by default — each scoped client opens its own httpx.Client. If you're fanning out across many merchants and care about pool reuse, pass a shared httpx.Client to the platform constructor; see below.
on_behalf_of is silently ignored for non-platform keys — the server will return 403 insufficient_scope, which surfaces as PlugipayError with code='insufficient_scope'.
Timeouts
plug = PlugipayClient(
key_id=..., secret=...,
timeout=10.0, # seconds; default is 30
)
The timeout applies to each individual request, not the lifetime of the client. When a request exceeds it, the SDK raises PlugipayTimeoutError.
Picking a value:
- Default 30s is fine for almost everything — reads typically return in 100-300ms, writes in 500ms-2s.
- Lower (5-10s) if you're in a user-facing request path and would rather fail fast and retry async.
- Higher (60s+) for
payouts.createagainst managed Xendit (the upstream rate-limits and we wait synchronously).
If you need different timeouts for different calls, build multiple clients with different timeout values rather than mutating one. Or pass a custom httpx.Client with finer-grained timeout config; see below.
Custom httpx client
For advanced needs — HTTPS proxies, mTLS, fine-grained timeouts, custom retries, observability hooks — pass your own httpx.Client:
import httpx
from plugipay import PlugipayClient
http = httpx.Client(
timeout=httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=2.0),
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
proxies="http://corporate-proxy:3128",
verify="/etc/ssl/certs/ca-bundle.crt",
)
plug = PlugipayClient(
key_id="pk_live_xxx",
secret="sk_live_xxx",
http=http,
)
When you pass http=, you own its lifecycle — plug.close() and the with block will NOT close it. That's deliberate: you can share one httpx.Client across many PlugipayClient instances (e.g., one per merchant in a partner scenario), and close it once when your process shuts down.
http = httpx.Client(...)
try:
plat = PlugipayClient(key_id="…", secret="…", http=http)
for merchant in merchants:
plat.for_merchant(merchant).invoices.list()
finally:
http.close()
The timeout= argument on PlugipayClient is still applied per-request (passed to http.request(..., timeout=...)); the timeouts on the httpx.Client are the fallback / connect-time budgets.
Sync vs async
PlugipayClient is synchronous. It blocks the current thread while a request is in flight.
If you need to call Plugipay from asyncio code, run it in a thread:
import asyncio
from plugipay import PlugipayClient
plug = PlugipayClient(key_id="…", secret="…")
async def create_customer_async(email: str):
return await asyncio.to_thread(plug.customers.create, email=email)
A first-class AsyncPlugipayClient is on the roadmap; the wire protocol and method surface will be identical — only async/await keywords on the methods. Until then, the thread-pool wrapper is the recommended pattern and matches what most other Python payments SDKs do today.
Connecting to a self-hosted Plugipay
If you're running Plugipay on your own infrastructure (open-source self-host), set base_url:
plug = PlugipayClient(
key_id="pk_live_xxx",
secret="sk_live_xxx",
base_url="https://plugipay.internal.example.com",
)
Trailing slashes are normalised. The SDK appends /api/v1/... per resource path — you don't include /api/v1 in base_url.
Verifying credentials
The fastest way to confirm a key works:
profile = plug.account.get()
print(profile["accountId"], profile.get("brandName"))
That hits GET /api/v1/account — one of the cheapest authenticated endpoints. On success you'll see your workspace's accountId (acc_…) and brand name. On failure you'll get PlugipayError with status=401 and a code of invalid_signature, invalid_key, invalid_timestamp, or mode_mismatch — each pointing at a specific fix. See API authentication → Common errors.
Next
- Errors — the
PlugipayErrorhierarchy. - API authentication — the HMAC recipe under the hood.
- API keys — mint, rotate, revoke keys.