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 revoke call itself takes effect immediately on the server side — your next request (including a follow-up list to verify) will fail. Always rotate by create 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_… or ak_test_…).
  • key.get("secret") — present only on the create response.
  • 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

Plugipay — Payments that don't tax your success