API keys
An API key is the HMAC credential pair (keyId + secret) you use to sign requests to Plugipay. You typically have a small handful per workspace — one or two for backend services, one for CI test-mode, maybe one scoped key for a specific tool. The Python SDK exposes three methods behind plugipay.api_keys. For the full key model (scopes, livemode vs testmode, key prefixes), see API → API keys.
Namespace
plug.api_keys # _ApiKeys
Methods
list
plug.api_keys.list() -> list[ApiKey]
Returns every API key registered against this workspace. Not paginated — keys are few. Returns a plain list, not a PageResult. The secret field is never returned here (see create).
for key in plug.api_keys.list():
print(key["id"], key.get("description"), key.get("scope"))
create
plug.api_keys.create(
*,
description: str | None = None,
scope: str | None = None,
) -> ApiKey
Mints a new API key. description is your label. scope is a Plugipay scope expression (e.g. "plugipay:customer:read") — omit for a full-power key inheriting the caller's scope. SDK auto-generates an idempotency key.
The response includes the secret exactly once. Store it in your secret manager immediately. Subsequent list calls will not return the secret — your only path to recover from a lost secret is revoke + create.
new_key = plug.api_keys.create(
description="ci-test-mode-runner",
scope="plugipay:read",
)
print(new_key["id"]) # "ak_live_..." or "ak_test_..."
print(new_key["secret"]) # The HMAC secret — STORE THIS NOW.
revoke
plug.api_keys.revoke(key_id: str) -> None
Permanently disables the key. Returns None on success. Any future request signed with the revoked key will fail with 401 unauthorized. There is no "un-revoke" — to restore access, create a new key.
plug.api_keys.revoke("ak_live_01HX...")
Don't revoke the key you're currently signing with. The
revokecall itself takes effect immediately on the server side — your next request (including a follow-uplistto verify) will fail. Always rotate bycreate new → cut over consumers → revoke old, in that order.
Types
from plugipay import ApiKey
ApiKey is a Resource subclass. Likely-read fields:
key["id"]— the full key id including livemode prefix (ak_live_…orak_test_…).key.get("secret")— present only on thecreateresponse.key.get("description")— your label.key.get("scope")— scope expression, or absent for full-power.key.get("status")—"active"or"revoked".key.get("lastUsedAt")— ISO 8601 UTC, useful for spotting dead keys.key["createdAt"].
Full reference at API → API keys.
Common patterns
Rotation script. The canonical safe rotation:
def rotate_key(plug, old_key_id, *, description, scope=None):
# 1. Mint the new one
new_key = plug.api_keys.create(description=description, scope=scope)
secret_manager.write(
path=f"plugipay/{description}",
data={"id": new_key["id"], "secret": new_key["secret"]},
)
# 2. Roll new credentials to consumers — usually via your config/deploy pipeline.
deploy_new_credentials(new_key["id"], new_key["secret"])
wait_for_consumers_to_pick_up()
# 3. Now safe to revoke
plug.api_keys.revoke(old_key_id)
return new_key
Dead-key cleanup. Detect keys nobody is using and revoke them:
from datetime import datetime, timedelta, timezone
cutoff = (datetime.now(timezone.utc) - timedelta(days=90)).isoformat()
for key in plug.api_keys.list():
last_used = key.get("lastUsedAt")
if not last_used or last_used < cutoff:
print(f"REVOKE {key['id']} — last used {last_used or 'never'}")
# plug.api_keys.revoke(key["id"]) # uncomment when satisfied
Scoped keys for least-privilege tools. A read-only reporting tool only needs plugipay:read:
report_key = plug.api_keys.create(
description="grafana-metrics-pull",
scope="plugipay:read",
)
# Push secret to Grafana via your secret manager.
A signing-only webhook responder might need plugipay:webhook:read. See API → Authentication for the scope catalog.
Test-mode vs live-mode. The id prefix tells you which mode: ak_live_… is live, ak_test_… is sandbox. Sign with the right one or the request fails:
# In production
plug = PlugipayClient(
key_id=os.environ["PLUGIPAY_LIVE_KEY_ID"],
secret=os.environ["PLUGIPAY_LIVE_KEY_SECRET"],
)
# In CI / staging
plug = PlugipayClient(
key_id=os.environ["PLUGIPAY_TEST_KEY_ID"],
secret=os.environ["PLUGIPAY_TEST_KEY_SECRET"],
)
Plugipay enforces strict separation — live keys don't see test data and vice versa.
Audit logging. Pair list with last-used timestamps in your weekly ops review:
print(f"{'id':<32} {'description':<30} {'lastUsedAt'}")
for key in plug.api_keys.list():
print(f"{key['id']:<32} {key.get('description', '-'):<30} {key.get('lastUsedAt', '-')}")
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Bad scope expression. |
404 |
not_found |
Key id doesn't exist or is in another workspace. |
409 |
idempotency_key_reused |
Same Idempotency-Key against a different body. |
403 |
insufficient_scope |
Key lacks plugipay:apikey:write (most workspace keys do — you typically need an admin key for these ops). |
401 |
unauthorized |
Signed with a revoked key — common on a request after a rotation if you missed a consumer. |
All raise PlugipayError. See Errors.
Next
- API → API keys — scope catalog and key id formats.
- Authentication — how the SDK uses these keys to sign requests.
- API → Authentication — the HMAC recipe and scope expression syntax.