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.keysynchronously. 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_scopescope catalog. - Audit log — who minted or revoked which key, when, and from where.