API keys
An API key is the HMAC (KeyID, Secret) pair you give to a service so it can sign Plugipay requests. Keys are scoped (read-only, write, admin) and revocable; rotating one means create-new + revoke-old without breaking in-flight traffic. The Go SDK exposes the three key-management methods behind client.ApiKeys. For wire shapes, scope strings, and rotation guidance, see API → API keys.
Field on the Client
client.ApiKeys — type *plugipay.ApiKeysResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
This namespace mints credentials. Use it with a workspace-admin key, not a per-service key. Revoke any key you mint via CI before shipping the code; the secret is shown exactly once.
Methods
List
Signature. func (r *ApiKeysResource) List(ctx context.Context) ([]ApiKey, error)
Returns every key in the workspace. The keys are typically small in number, so the method returns a plain slice rather than a paginated Page[T]. The Secret field is empty on list — it's only echoed at create time.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
keys, err := client.ApiKeys.List(ctx)
if err != nil {
return err
}
for _, k := range keys {
revoked := "active"
if k.RevokedAt != nil { revoked = "revoked@" + *k.RevokedAt }
log.Printf("%s keyId=%s scope=%s desc=%v %s",
k.ID, k.KeyID, k.Scope, k.Description, revoked)
}
Create
Signature. func (r *ApiKeysResource) Create(ctx context.Context, in ApiKeyCreateInput) (*ApiKey, error)
Mints a new key. Both fields are optional — Description is free-text for your own bookkeeping; Scope defaults to read-only when omitted. Auto-keyed for idempotency.
desc := "ci-runner-2026-05"
scope := "plugipay:checkout:write,plugipay:customer:read"
k, err := client.ApiKeys.Create(ctx, plugipay.ApiKeyCreateInput{
Description: &desc,
Scope: &scope,
})
if err != nil {
return err
}
// Save k.Secret right now — it's only returned once.
storeSecret(k.KeyID, k.Secret)
log.Printf("minted %s (keyId=%s)", k.ID, k.KeyID)
Save
Secretimmediately. It's only returned onCreate. SubsequentListcalls echo every field exceptSecret. If you lose it, revoke the key and mint a new one.
Revoke
Signature. func (r *ApiKeysResource) Revoke(ctx context.Context, id string) error
Disables the key. Subsequent signed requests with that KeyID fail with *plugipay.Error{Status: 401, Code: "invalid_key"}. The record stays in List with RevokedAt set, so you have an audit trail. Idempotent — revoking an already-revoked key returns nil.
if err := client.ApiKeys.Revoke(ctx, "apk_01HX..."); err != nil {
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "not_found" {
return nil // already gone
}
return err
}
There's no Update — scope changes are revoke-and-recreate.
Types
type ApiKey struct {
ID string `json:"id"` // "apk_..."
AccountID string `json:"accountId"`
KeyID string `json:"keyId"` // "ak_live_..." or "ak_test_..."
Description *string `json:"description"`
Scope string `json:"scope"` // comma-joined scopes
Secret string `json:"secret,omitempty"` // only on Create
CreatedAt string `json:"createdAt"`
RevokedAt *string `json:"revokedAt"`
}
Scope catalog (e.g. plugipay:checkout:write, plugipay:platform:admin): API → API keys.
Common patterns
Provision a per-service key on first deploy
func ensureKey(ctx context.Context, c *plugipay.Client, desc, scope string) (string, string, error) {
keys, err := c.ApiKeys.List(ctx)
if err != nil { return "", "", err }
for _, k := range keys {
if k.RevokedAt == nil && k.Description != nil && *k.Description == desc {
// active key with this description already exists
return k.KeyID, "", nil // secret unknown to us at this point
}
}
k, err := c.ApiKeys.Create(ctx, plugipay.ApiKeyCreateInput{
Description: &desc,
Scope: &scope,
})
if err != nil { return "", "", err }
return k.KeyID, k.Secret, nil
}
If secret == "" came back from the list-only branch, you've hit the lost-secret case — the key exists but we never persisted its secret. Revoke and re-create.
Rotating a production key
The safe rotation flow keeps both keys live during the window your services flip:
// 1) Mint the replacement
newKey, err := c.ApiKeys.Create(ctx, plugipay.ApiKeyCreateInput{
Description: ptr("prod-2026-Q3"),
Scope: ptr(oldKey.Scope),
})
if err != nil { return err }
if err := pushSecret(newKey); err != nil { return err }
// 2) Roll services to new credentials (gradual deploy)
// ... wait until metrics on the old key drop to zero ...
// 3) Revoke the old one
if err := c.ApiKeys.Revoke(ctx, oldKey.ID); err != nil { return err }
Plugipay's HMAC verification is constant-time per-key — running two active keys has no security impact.
Audit "who's still using the old key"
There's no per-key request counter in the SDK; the answer comes from the event log or your own observability. The cheap-and-easy way: rotate, then check your error rate on invalid_key for a few days — spikes mean a service is still on the old credentials.
Errors with errors.As
err := c.ApiKeys.Revoke(ctx, id)
var pe *plugipay.Error
if errors.As(err, &pe) {
switch pe.Code {
case "not_found": // already revoked or never existed
case "insufficient_scope": // key you're authenticating with isn't admin
}
}
Context cancellation
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
k, err := c.ApiKeys.Create(ctx, in)
Create is auto-keyed for idempotency — if *plugipay.Error{Code: "timeout"} comes back, retry on a fresh ctx and you'll get the same key (with the same secret) instead of two.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Unknown scope string, malformed scope set. |
not_found |
404 | Revoke / get on a missing id. |
conflict |
409 | Same idempotency key with a different body. |
insufficient_scope |
403 | Caller key lacks plugipay:platform:admin (or the workspace-level admin scope). |
Full mechanics: Errors.
Next
- Authentication — how the SDK signs with
(KeyID, Secret). - Errors —
invalid_key/invalid_signaturetriage. - API → API keys — HTTP-level reference.