Seals
Concept: Seals & integrity.
Personal seal — /api/v1/seals (authenticated)
Section titled “Personal seal — /api/v1/seals (authenticated)”| Method · Route | Purpose |
|---|---|
POST / | Register a seal. Body: ownerType (User or Organization, default User), tier (security tier), orgSlug?. The owner is always the authenticated user. 400 with the result body when registration fails. |
DELETE /{credentialId}?ownerType= | Revoke one of the caller’s seals. Must be called from a sealed device. |
POST /check | Pre-flight before registering; same body as POST /. Always 200 with isSuccess; errorCode = "SEAL_001" when the account cannot be found. |
GET /check-hybrid-binding?orgSlug= | Whether the caller has a pending company-device enrollment in that org (hasPendingToken, declaredDeviceClass), and whether the org requires device-bound passkeys. Answers only for a person in that org (active, invited, or accepted and awaiting their seal); anyone else gets the same answer as for an unknown org. |
POST / accepts an optional orgSlug. It is used only when the caller is a person in that org (active, invited, or accepted and awaiting their seal); otherwise the seal is still registered for the caller, with no org attached.
Seal registry — GET /api/org/{slug}/seals
Section titled “Seal registry — GET /api/org/{slug}/seals”The caller must be an active member of the org (or an Owner/Admin of an ancestor org); anyone else gets 403 with errorCode = "ORG_NOT_MEMBER". The slug is matched case-insensitively. Within the org the route requires the view.seals page scope: the Owner and Admin bypass it, any other member must hold it, and a missing scope returns 403 with errorCode = "PERMISSION_DENIED".
Returns the org’s seals:
- Master seal — one row if the organization has a master seal (
"{org} Master Seal", scopeALL (ROOT)). - Personal seals — each active member’s passkeys surfaced as personal seals (one row per passkey; the first is the semantic personal seal).
Passkey change history and alerts
Section titled “Passkey change history and alerts”Every time a passkey (a personal seal’s public key) is added to or removed from an account, Axowl records it in three places. The key fingerprint is the first 16 hex characters of the SHA-256 of the stored public key, and it is the same in all three, so a member can compare what they were told with what the records show.
| Where | Who sees it | What it shows |
|---|---|---|
| Security email to the account address | The member | Time, device (on add), key ID, key fingerprint. Sent for a new passkey (api/passkey/add) and for every removal, including a removal caused by deleting a device. Binding a seal key ID (POST /api/v1/seals) does not send one. |
GET /api/auth/activity-log (profile Activity Log) | The member | Rows with eventType seal.lifecycle.registered / seal.lifecycle.revoked, plus keyId and keyFingerprint. Login and logout rows return null for both. |
GET /api/v1/admin/audit/query?eventTypes=seal.lifecycle.registered,seal.lifecycle.revoked&scope=all (Seal Registry ▸ Change history) | Org Owners and Admins, Pro plan or higher | The org’s members’ passkey changes. Each row’s businessData carries CredentialId and PublicKeyFingerprint. Same plan and role gate as any org-wide audit query. |
Adding one passkey writes two seal.lifecycle.registered records: one for the new public key (NewPublicKey: true) and one when the seal key ID is bound to it (NewPublicKey: false). Only the first sends an email or appears in either history. Records written before 2026-10-16 have no NewPublicKey value and are all shown.
The audit record is written inside Axowl; the email lands outside it. If a stored public key were swapped, the member either receives an email for a change they did not make, or the fingerprint in their email no longer matches the record.
Bind personal → master (sole proprietorship)
Section titled “Bind personal → master (sole proprietorship)”POST /api/org/{slug}/master-seal/bind-personal — for an independent sole proprietorship whose Owner had no passkey at org-creation time. After the Owner registers a passkey, this binds their first personal seal as the org master seal.
| Status | When |
|---|---|
400 | The org is not an independent sole proprietorship, or the Owner has no personal seal yet |
403 | The caller is not the org’s active Owner |
409 | A master seal is already set (seals are immutable) |
Registering a personal seal (passkey)
Section titled “Registering a personal seal (passkey)”The dashboard walks a member through the personal-seal ritual in this order: platform-authenticator check → pre-flight (POST /api/v1/seals/check, skipped for device-only enrollment) → api/passkey/add/options → navigator.credentials.get → api/passkey/add → POST /api/v1/seals. The first passkey is the semantic Personal Seal, and registering it also activates a Pending membership → Active. Hybrid device-trust enrollment tokens ride the same flow. This is a required onboarding step and the member entry-gate (see Seals & integrity).
Visual seals (the designed stamp)
Section titled “Visual seals (the designed stamp)”Distinct from the cryptographic seals above — these are the rendered SVG stamps drawn on approval documents. Full model: Seals & integrity → the visual seal designer.
| Method · Route | Purpose |
|---|---|
POST /api/org/{slug}/settings/seal | Upload or replace the company seal image. Requires org.setting.update. Returns { sealUrl }. |
DELETE /api/org/{slug}/settings/seal | Remove the company seal. Requires org.setting.update. |
PUT /api/org/{slug}/members/{connectedId}/signature | Set the caller’s own personal signature/seal — { kind, spec } where kind = signature / seal / draw / image / none. Self-only; spec is JSON of at most 8 KB. Emits org.member.signature_changed (ORG025). |
POST /api/org/{slug}/members/{connectedId}/signature/image | Upload a raster or SVG personal seal image. Self-only. |
The designed spec is returned by /me as signatureSpec and rendered wherever a seal appears.