Keys and abilities
The Content API authenticates requests with Bearer keys minted from the panel. Each key carries a set of abilities that scope which discovered resources it may touch. Keys are hashed at rest; the plaintext is shown once.
Keys
Keys are created on the API settings page in the panel. ApiKey::mint() generates a random token, prefixes it with agk_, and stores only a SHA-256 hash plus a 12-character display prefix:
$plain = 'agk_'.bin2hex(random_bytes(24));
$key = ApiKey::create([
'prefix' => substr($plain, 0, 12),
'key_hash' => hash('sha256', $plain),
'abilities' => $abilities ?? ContentApi::abilities(),
]);
The plaintext is returned to the caller exactly once and never stored. Lookups on each request re-hash the incoming Bearer token and match it against key_hash (ApiKey::findByPlain()). Store the plaintext in your secret manager immediately after minting.
Abilities
Every discovered resource exposes four abilities, shaped {prefix}:create|read|update|delete. The prefix is the basename of the resource slug (Str::of($class::getSlug())->basename()), so a BlogPostResource with slug blog-posts yields blog-posts:create, blog-posts:read, blog-posts:update, blog-posts:delete.
ApiKey::allows($ability) returns true if the ability is in the key's abilities array. A key with null abilities bypasses the ability layer — useful for a master key, but scope keys down in production.
Discovery
Abilities are not hand-written. ContentApi::discover($panel) walks $panel->getResources(), skips anything in the opt-out list, and builds the ability set automatically. Add a new Filament resource and its four abilities appear on the next key mint.
Opt-out covers the panel's own infrastructure resources: UserResource, RoleResource, MediaResource, plus any model that extends Authenticatable. ContentApi::except() lets you exclude more, and ContentApi::resource() registers a model that has no Filament resource under a chosen prefix.
Authentication flow
The content.api middleware stack resolves the request to a user before the ability check runs:
- Bearer
agk_key — hash lookup, thenmarkUsed(), then authenticate as the key's owner. - Env
API_KEY— single shared secret, impersonates the user configured atcontent-api.key_user. Compared withhash_equals. - Passport token — falls through to Laravel's
apiguard. Valid, but unscoped at the key layer; use a hashedagk_key when you need per-resource abilities.
After auth, EnsureContentApiAbility calls ContentApi::ensureAbility($ability), which abort_unlesses 403 when the key lacks the required ability. Authorization then defers to the same Eloquent policies the panel uses.
Ability reference
| Ability | Allows |
|---|---|
posts:create |
POST /api/v1/posts |
posts:read |
GET /api/v1/posts, GET /api/v1/posts/{id} |
posts:update |
PATCH /api/v1/posts/{id} |
posts:delete |
DELETE /api/v1/posts/{id} |
Mint and request flow
sequenceDiagram
actor Editor
participant Panel as API settings
participant DB
participant API as Content API
participant Policy
Editor->>Panel: Mint key (select abilities)
Panel->>DB: store key_hash + abilities
Panel-->>Editor: show plaintext once
Editor->>API: Bearer agk_…
API->>DB: hash compare (findByPlain)
API->>API: markUsed() + authenticate as owner
API->>API: EnsureContentApiAbility(prefix:action)
API->>Policy: authorize (Eloquent policy)
Policy-->>API: allowed
API-->>Editor: 2xx + JSON