API keys

The API keys resource lets you mint, list, and revoke HMAC credentials programmatically — the same keys you'd otherwise create from Dashboard → Settings → API keys.

This page is about managing keys. For the signing recipe, see Authentication; for the portal walkthrough, see Portal → API keys.

The secret is shown exactly once, on creation. Plugipay stores only a one-way hash. If you lose the secret, the only recovery is to revoke the key and mint a new one — there is no fetch-secret endpoint and there never will be.

Endpoints

Method Path Operation
POST /v1/api-keys Create a key
GET /v1/api-keys/{id} Retrieve a key
GET /v1/api-keys List keys
DELETE /v1/api-keys/{id} Revoke a key

The caller needs full_access or a restricted role with apiKeys:write to create/revoke. read_only keys can list and retrieve.

Create a key

POST /v1/api-keys

Mints a new key under the caller's workspace and returns the plaintext secret. It is not retrievable later.

Request body

Field Type Required Notes
name string yes Human label, 1–80 chars. Use something specific (Production server, CI — GitHub Actions).
environment string no test (default) or live. Locks the key prefix; immutable.
role string no full_access (default), read_only, webhook_management_only, or restricted. See Roles.
scopes string[] no Only for role: "restricted". Explicit scope list, e.g. ["payments:read", "refunds:write"].

Response

201 Created. The response object includes every API key field plus a one-time key field containing the plaintext secret.

{
  "data": {
    "id": "akey_01HXxxxxxxxxxxxxxxxxxxxxxx",
    "keyId": "pk_live_a1b2c3d4e5f6",
    "name": "Production server",
    "role": "full_access",
    "environment": "live",
    "lastUsedAt": null,
    "createdAt": "2026-05-12T10:42:00.123Z",
    "revokedAt": null,
    "key": "pk_live_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890"
  },
  "error": null,
  "meta": { "requestId": "req_01H...", "timestamp": "2026-05-12T10:42:00.123Z" }
}

data.key is the HMAC secret. It is present only on this 201 response; subsequent GET calls never include it.

Capture data.key synchronously. Pipe it straight to your secrets manager or environment file. Don't log it; don't keep it in shell history.

Code samples

// Node — @forjio/plugipay
const created = await plugipay.apiKeys.create({
  name: 'Production server',
  environment: 'live',
  role: 'full_access',
});
console.log(created.keyId);     // pk_live_a1b2c3d4e5f6
console.log(created.key);       // pk_live_a1b2…  (only here, only now)
# Python — plugipay
created = client.api_keys.create(
    name="Production server",
    environment="live",
    role="full_access",
)
print(created.key_id)   # pk_live_a1b2c3d4e5f6
print(created.key)      # full secret, only on this call
// Go — github.com/hachimi-cat/plugipay-go
created, err := client.ApiKeys.Create(ctx, plugipay.ApiKeyCreate{
    Name:        "Production server",
    Environment: "live",
    Role:        "full_access",
})
if err != nil { return err }
fmt.Println(created.KeyID, created.Key)
# curl — using the signing helper from /docs/api/authentication
plugipay_curl POST '/v1/api-keys' \
  '{"name":"Production server","environment":"live","role":"full_access"}'

Errors

Status error.code When
400 validation_error Missing/oversized name; bad environment/role; scopes set without role: "restricted".
403 insufficient_scope Caller key lacks apiKeys:write.
409 tier_cap_reached Workspace hit its maxApiKeys plan limit. Revoke a stale key or upgrade.

Retrieve a key

GET /v1/api-keys/{id}

Returns metadata for a single key. The secret is never included in this response.

const key = await plugipay.apiKeys.get('akey_01HX...');
key = client.api_keys.get("akey_01HX...")
key, err := client.ApiKeys.Get(ctx, "akey_01HX...")
plugipay_curl GET '/v1/api-keys/akey_01HXxxxxxxxxxxxxxxxxxxxxxx'

Errors

Status error.code When
404 not_found No key with that ID, or it belongs to a different workspace.

List keys

GET /v1/api-keys

Returns every non-revoked key in the caller's workspace, newest first.

Query parameters

Param Type Notes
environment string Filter by test or live. Default: both.
includeRevoked boolean If true, returns revoked keys with revokedAt populated. Default false.
limit integer 1–100. Default 50.
cursor string Pagination cursor from a previous response.

Response

{
  "data": [
    {
      "id": "akey_01HX...",
      "keyId": "pk_live_a1b2c3d4e5f6",
      "name": "Production server",
      "role": "full_access",
      "environment": "live",
      "lastUsedAt": "2026-05-12T10:41:00.000Z",
      "createdAt": "2026-04-22T08:13:00.000Z",
      "revokedAt": null
    }
  ],
  "error": null,
  "meta": { "requestId": "...", "timestamp": "...", "page": { "limit": 50, "hasMore": false, "nextCursor": null } }
}
const { data } = await plugipay.apiKeys.list({ environment: 'live' });
keys = client.api_keys.list(environment="live")
keys, err := client.ApiKeys.List(ctx, plugipay.ApiKeyList{Environment: "live"})
plugipay_curl GET '/v1/api-keys?environment=live'

Revoke a key

DELETE /v1/api-keys/{id}

Revokes a key immediately. Any request signed with it starts returning 401 invalid_key within a few seconds — no grace period. Returns 204 No Content (envelope with data: null).

await plugipay.apiKeys.revoke('akey_01HX...');
client.api_keys.revoke("akey_01HX...")
err := client.ApiKeys.Revoke(ctx, "akey_01HX...")
plugipay_curl DELETE '/v1/api-keys/akey_01HXxxxxxxxxxxxxxxxxxxxxxx'

Errors

Status error.code When
404 not_found No such key in this workspace.
409 cannot_revoke_self You tried to revoke the key signing the current request. Mint a replacement and revoke from the new key.

Revocation is irreversible and propagates within seconds. Always rotate before revoking — never the other way round.

The API key object

Field Type Notes
id string Internal ID with prefix akey_. Use this in URLs.
keyId string Public access key id, e.g. pk_live_a1b2c3d4e5f6. The environment prefix plus 12 hex chars. Safe to log.
name string Whatever you passed on create.
role string One of the four roles.
environment string test or live. Immutable.
lastUsedAt string or null ISO 8601 timestamp of the most recent signed request. null until first use. Updates within seconds.
createdAt string ISO 8601 creation timestamp.
revokedAt string or null ISO 8601 revocation timestamp; null for live keys. Only populated when includeRevoked=true.
key string Create-only. The plaintext secret. Present on 201 Created and nowhere else.

keyId goes in the Authorization header; key is the HMAC key. See Authentication.

Roles

Pick the narrowest role that works. Roles can't be widened — mint a new key instead.

Role What it allows
full_access Every endpoint in the workspace. Default.
read_only All GET endpoints. No writes.
webhook_management_only Read/write on /v1/webhook-endpoints and /v1/events. Nothing else. Useful for a deploy-time registration job.
restricted Caller-defined scope list via scopes: ["payments:read", "refunds:write", ...] on create. See Errors for the scope catalog.

A 403 insufficient_scope from any endpoint means the calling key's role is too narrow for that operation.

Programmatic rotation

Always: mint → verify → cut over → revoke. Never the reverse.

// 1. Mint a replacement with the same role + environment.
const next = await plugipay.apiKeys.create({
  name: `Production server (rotated ${new Date().toISOString().slice(0, 10)})`,
  environment: 'live',
  role: 'full_access',
});

// 2. Push next.key into your secrets manager. Wait for consumers
//    to reload and confirm they sign one request successfully.
await secrets.set('PLUGIPAY_KEY_ID', next.keyId);
await secrets.set('PLUGIPAY_KEY_SECRET', next.key);
await waitForConsumersToReload();

// 3. Once lastUsedAt advances on the new key (and the old one has
//    gone quiet), revoke the old key.
await plugipay.apiKeys.revoke(process.env.OLD_KEY_INTERNAL_ID);

lastUsedAt is the simplest verification signal — if it advances on the new key within 60 seconds of cutover, you're safe to revoke. If not, the old key is still in use; investigate first. Schedule this as a quarterly cron per environment.

Events

The API keys resource is intentionally not broadcast on the event stream — we don't want webhook subscribers enumerating or correlating credential lifecycle. Key creation and revocation do show up in the audit log under the acting key. Capture a signal in your own systems at the point you call apiKeys.create / apiKeys.revoke.

Next

  • Authentication — using the secret you just minted to sign requests.
  • Portal → API keys — the human-facing equivalent, with best-practice notes.
  • Errors — the insufficient_scope scope catalog.
  • Audit log — who minted or revoked which key, when, and from where.
Plugipay — Payments that don't tax your success