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 Secret immediately. It's only returned on Create. Subsequent List calls echo every field except Secret. 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

Plugipay — Payments that don't tax your success