Headlessly
Identity

ApiKey

Programmatic access tokens for SDK, CI/CD, and agent authentication.

Schema

import { Noun } from 'digital-objects'

export const ApiKey = Noun('ApiKey', {
  name: 'string!',
  keyPrefix: 'string!##',
  scopes: 'string',
  status: 'Active | Revoked | Expired',
  revoke: 'Revoked',
})

Fields

FieldTypeRequiredDescription
namestringYesHuman-readable label for the key (e.g. "GitHub Actions", "Production Agent")
keyPrefixstringYesFirst 8 characters of the key (unique, indexed) -- used for identification without exposing the full secret
scopesstringNoComma-separated permission scopes (e.g. read:all,write:content)
statusenumNoKey state: Active, Revoked, or Expired

Verbs

VerbEventDescription
createCreatedGenerate a new API key
updateUpdatedUpdate key metadata (name, scopes)
deleteDeletedSoft-delete the key
revokeRevokedPermanently disable the key

Verb Lifecycle

Every verb follows the full conjugation pattern -- execute, before hook, after hook:

import { ApiKey } from '@headlessly/sdk'

// Execute
await ApiKey.revoke('apiKey_k7TmPvQx')

// Before hook -- runs before revocation is processed
ApiKey.revoking(key => {
  console.log(`About to revoke key ${key.name} (${key.keyPrefix}...)`)
})

// After hook -- runs after revocation completes
ApiKey.revoked(key => {
  console.log(`Key ${key.keyPrefix}... revoked at ${key.$updatedAt}`)
})

Status State Machine

  create()
(none) ──────→ Active

        revoke()  │  (TTL expiry)

            Revoked / Expired

Valid transitions:

FromVerbTo
--createActive
ActiverevokeRevoked
Active(system)Expired

Revocation is permanent -- a revoked or expired key cannot be reactivated. Create a new key instead.

Query Examples

SDK

import { ApiKey } from '@headlessly/sdk'

// Find all active keys
const activeKeys = await ApiKey.find({ status: 'Active' })

// Get a specific key by ID
const key = await ApiKey.get('apiKey_k7TmPvQx')

// Count keys by status
const revokedCount = await ApiKey.count({ status: 'Revoked' })

MCP

headless.ly/mcp#search
{ "type": "ApiKey", "filter": { "status": "Active" } }
headless.ly/mcp#fetch
{ "type": "ApiKey", "id": "apiKey_k7TmPvQx" }
headless.ly/mcp#do
const keys = await $.ApiKey.find({ status: 'Active' })
await $.ApiKey.revoke('apiKey_k7TmPvQx')

REST

GET https://headless.ly/~acme/api-keys?status=Active
GET https://headless.ly/~acme/api-keys/apiKey_k7TmPvQx
POST https://headless.ly/~acme/api-keys/apiKey_k7TmPvQx/revoke

Event-Driven Patterns

React to API key lifecycle events:

import { ApiKey } from '@headlessly/sdk'

// Log all key creation for audit trail
ApiKey.created((key, $) => {
  $.Event.create({
    type: 'security.api_key_created',
    data: { name: key.name, keyPrefix: key.keyPrefix, scopes: key.scopes },
  })
})

// Alert on key revocation
ApiKey.revoked((key, $) => {
  $.Event.create({
    type: 'security.api_key_revoked',
    data: { name: key.name, keyPrefix: key.keyPrefix },
  })
})

Security Notes

  • The full API key secret is only returned once at creation time -- store it securely
  • Use keyPrefix to identify keys in logs and dashboards without exposing the secret
  • Scope keys to the minimum permissions needed: read:crm, write:content, admin:billing
  • Rotate keys regularly -- create a new key, update your systems, then revoke the old one

On this page