Skip to content
AXOWLDevelopers
Quickstart

Seals

Concept: Seals & integrity.

Personal seal — /api/v1/seals (authenticated)

Section titled “Personal seal — /api/v1/seals (authenticated)”
Method · RoutePurpose
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 /checkPre-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", scope ALL (ROOT)).
  • Personal seals — each active member’s passkeys surfaced as personal seals (one row per passkey; the first is the semantic personal seal).

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.

WhereWho sees itWhat it shows
Security email to the account addressThe memberTime, 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 memberRows 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 higherThe 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.

StatusWhen
400The org is not an independent sole proprietorship, or the Owner has no personal seal yet
403The caller is not the org’s active Owner
409A master seal is already set (seals are immutable)

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).

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 · RoutePurpose
POST /api/org/{slug}/settings/sealUpload or replace the company seal image. Requires org.setting.update. Returns { sealUrl }.
DELETE /api/org/{slug}/settings/sealRemove the company seal. Requires org.setting.update.
PUT /api/org/{slug}/members/{connectedId}/signatureSet 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/imageUpload a raster or SVG personal seal image. Self-only.

The designed spec is returned by /me as signatureSpec and rendered wherever a seal appears.