Contents

API keys and audit log

API keys and the audit log.

The ApiKey object

Attributes

  • id required Type: string
  • display_prefix required Type: string

    The key's first characters, to recognise it.

  • name required Type: string
  • permissions required Type: Permission[]
  • created_at required Type: string
  • last_used_at required Type: string null
  • revoked_at required Type: string null
  • created_by optional Type: string null

    Who made the key, e.g. user:<id>.

  • owner optional Type: KeyOrg

    The sub-organisation that owns a key you issued for it.

  • issued_by optional Type: KeyOrg

    The parent organisation that issued this key for you.

ApiKey on its own page

List API keys

GET /keys

Needs org:keys:manage: Create/revoke API keys (within own permissions)

Your keys, and the keys you issued for your sub-organisations, each with its owner.

Parameters

  • org_id queryoptional Type: string

    Only the keys owned by this organisation (yours, or a sub-organisation's you issued keys for).

Returns

  • 200
    The keys
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /keys · curl
curl "https://api.unforged.sh/keys" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /keys · Python
import requests

r = requests.get(
    "https://api.unforged.sh/keys",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /keys · Node.js
const res = await fetch('https://api.unforged.sh/keys', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

Issue an API key

POST /keys

Needs org:keys:manage: Create/revoke API keys (within own permissions)

The key is shown once. No Idempotency-Key: a retry issues another key.

Request body

ApiKeyCreate

  • name required Type: string
  • permissions required Type: Permission[]

    The key's permissions, at most your own.

  • org_id optional Type: string

    A sub-organisation that will own the key, so requests made with it run there with no Unforged-Org header. Defaults to your own organisation. Needs org:children:access, isn't available while acting in another organisation, and the key can't hold org:keys:manage, org:billing:manage or org:children:manage.

Returns

  • 201 Type: ApiKeyCreated
    Created
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 402 Type: Problem
    billing_required
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 413 Type: Problem
    too_large
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
POST /keys · curl
curl -X POST "https://api.unforged.sh/keys" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "backend",
  "permissions": [
    "nib:checks:create",
    "nib:checks:read",
    "nib:subjects:read"
  ]
}'
POST /keys · Python
import requests

r = requests.post(
    "https://api.unforged.sh/keys",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
    json={
        "name": "backend",
        "permissions": [
            "nib:checks:create",
            "nib:checks:read",
            "nib:subjects:read"
        ]
    },
)
print(r.json())
POST /keys · Node.js
const res = await fetch('https://api.unforged.sh/keys', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "name": "backend",
    "permissions": [
      "nib:checks:create",
      "nib:checks:read",
      "nib:subjects:read"
    ]
  }),
});
console.log(await res.json());
Response 201
{
  "id": "key_01j9x3m8v7k2q4r5s6t7u8v9w0",
  "display_prefix": "unf_live_01AbCdEf",
  "name": "backend",
  "permissions": [
    "nib:checks:create",
    "nib:checks:read",
    "nib:subjects:read"
  ],
  "created_at": "2026-09-25T10:15:30.123Z",
  "last_used_at": null,
  "revoked_at": null,
  "key": "unf_live_01AbCdEf…EXAMPLE"
}

Revoke an API key

DELETE /keys/{key_id}

Needs org:keys:manage: Create/revoke API keys (within own permissions)

Takes effect on the next request; 204 also when already revoked. 409 conflict for the last key with org:keys:manage.

Parameters

  • key_id pathrequired Type: string

Returns

  • 204
    Done
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 404 Type: Problem
    not_found
  • 409 Type: Problem
    conflict, idempotency_conflict, idempotency_in_progress or idempotency_not_replayable
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
DELETE /keys/{key_id} · curl
curl -X DELETE "https://api.unforged.sh/keys/{key_id}" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
DELETE /keys/{key_id} · Python
import requests

r = requests.delete(
    "https://api.unforged.sh/keys/{key_id}",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
DELETE /keys/{key_id} · Node.js
const res = await fetch('https://api.unforged.sh/keys/{key_id}', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());

The audit log, newest first

GET /audit

Needs org:audit:read: Audit log

Team and invitation changes are recorded with these actions (addresses are stored hashed in detail, never in clear): team.invite, team.invite_resend, team.invite_revoke, team.invite_accepted, team.invite_declined, team.role_change, team.member_remove, team.domain_add, team.domain_verify, team.domain_set, team.domain_remove, team.domain_join and team.admin_portal. Filter by one of these exact names with action=<name>.

Parameters

  • cursor queryoptional Type: string

    The previous page's next_cursor.

  • limit queryoptional Type: integer
  • action queryoptional Type: string

    An exact action, or a prefix ending in *.

Returns

  • 200 Type: AuditPage
    A page of entries
  • 400 Type: Problem
    invalid_request, invalid_version, idempotency_key_required or invalid_signature
  • 401 Type: Problem
    unauthorized
  • 403 Type: Problem
    permission_denied, org_suspended, or address_not_allowed (your organisation only accepts requests from the addresses it has listed)
  • 421 Type: Problem
    wrong_region
  • 429 Type: Problem
    rate_limited or quota_exceeded
  • default Type: Problem
    internal
GET /audit · curl
curl "https://api.unforged.sh/audit" \
  -H "Authorization: Bearer $UNFORGED_KEY" \
  -H "Unforged-Version: 2026-10-14"
GET /audit · Python
import requests

r = requests.get(
    "https://api.unforged.sh/audit",
    headers={"Authorization": f"Bearer {UNFORGED_KEY}", "Unforged-Version": "2026-10-14"},
)
print(r.json())
GET /audit · Node.js
const res = await fetch('https://api.unforged.sh/audit', {
  method: 'GET',
  headers: { Authorization: `Bearer ${process.env.UNFORGED_KEY}`, 'Unforged-Version': '2026-10-14' },
});
console.log(await res.json());
Response 200
{
  "entries": [
    {
      "id": 42,
      "actor": "api_key:key_01j9x3m8v7k2q4r5s6t7u8v9w0",
      "action": "policy.create",
      "target": "strict@2",
      "detail": {},
      "at": "2026-09-25T10:15:30.123Z"
    }
  ],
  "next_cursor": "42"
}