Vault
Credentials vault, zero-knowledge by default: encrypted sync, agent broker, approvals and device keys.
Kodwen Vault keeps your credentials encrypted on your own devices: clients seal every item (XChaCha20-Poly1305, keys wrapped with the X-Wing post-quantum hybrid) before upload, so by default /v1/vault/* stores and syncs ciphertext that Kodwen cannot read. The vault is zero-knowledge by default, with one exception that is off by default and set per item: the owner of a vault can mark an item usable in the cloud (PUT /v1/vault/items/{id}/cloud). The client then sends the decrypted item, and the gateway keeps a second copy sealed with a Kodwen KMS key, which it can open to serve agent requests (/v1/vault/http, /v1/vault/resolve, /v1/vault/derive) while your devices are offline. Only the owner of the vault can switch it on or off; switching it off, changing the item or deleting it removes that copy. Agents never receive secret values. They see a catalogue of handles ({{vault:<slug>}}), ask for a policy decision (tiers t0-t4 or deny), wait for your approval when the tier requires it, and call brokered HTTPS endpoints whose responses are redacted.
The SDKs expose the whole surface as codai.vault (TypeScript and Python), with device keys under codai.vault.devices.
Items: sync, writes and roles
Reading and syncing items
GET /v1/vault/items answers { items, server_time, has_more }. Each item is the stored row: id, vault_id, revision, wrapped_key, ciphertext, kind, slug, agent_meta, honeytoken, cloud_usable, deleted_at, last_used_at, created_at, updated_at.
- Full list (no
since): live items only, most recently changed first, at most 5000.has_moreis true when more than 5000 live items exist. - Changes (
?since=<ISO 8601 date-time>): every item changed aftersince, deleted items included (deleted_atis set: remove the item locally), oldest first, at most 1000 per call. - Deleted items: an item deleted more than 30 days ago keeps only a marker row and its
wrapped_keyandciphertextare empty strings, so a client must never decrypt a row that hasdeleted_at. has_more: true: call again at once withsince= theserver_timeyou received, which is then theupdated_atof the last item returned. Rely onhas_more, not on the number of items: a page is never cut inside one millisecond, so it can hold fewer than 1000 items, and it holds more than 1000 only when over 1000 items changed in the same millisecond.has_more: false: you are up to date.server_timeis the database clock, read before the items, minus 5 seconds; store it as thesinceof your next sync. It can be earlier than thesinceyou sent. An item changed in those last seconds can arrive again: replace your copy of thatid.- A
sincethat cannot be read as a date-time, an empty value included, answers 400invalid_since. Times are compared by the millisecond.
Writing items
PUT /v1/vault/items/{id} { vault_id, revision, wrapped_key, ciphertext, kind, slug?, agent_meta?, honeytoken? } answers 201 { id, revision } for a new item and 200 { id, revision } for an update. The body has no id: the path names the item, and a key the route does not know answers 400.
- A new item sends
revision: 1; an update sends the stored revision + 1. Anything else answers 409{ error: { code: 'revision_conflict' }, revision }with the stored revision: reload the item and retry. - 409
item_deleted: the item was deleted on another device and stays deleted. Remove it locally; to keep its content, create a new item with a new id. - The
kindand the vault of an item are fixed when it is created; a change answers 400. - Agent exposure (
slugandagent_meta, always sent together) andhoneytokenare decisions of the owner or a manager of the vault. An editor who creates an item with them, or changes them, gets 403. Every update sends the storedslug,agent_metaandhoneytokenback unchanged; leaving them out counts as a change. - A
slugalready used in the same vault answers 400. A content change removes the cloud copy of the item.
DELETE /v1/vault/items/{id} keeps the row as a deleted item for sync, revokes every grant on it and removes its cloud copy. Deleting an item that is already deleted answers the same 200 { deleted: true } and changes nothing.
Recording a local use
POST /v1/vault/items/{id}/used { via } records that a passkey or login was used on a device (via: windows_plugin, android_provider, ios_provider, desktop or phone) and answers { id, last_used_at }. Only the time is stored, never the contents. The route keeps one stamp per item per 60 seconds: a repeat inside that window answers 200 with the stored last_used_at, writes nothing and records no item.used event. It never answers 429. An unknown, deleted or inaccessible item answers 404.
What each role may do on items
| Action | viewer | editor | manager | owner |
|---|---|---|---|---|
Read and sync items (GET /v1/vault/items) | yes | yes | yes | yes |
Record a local use (POST /v1/vault/items/{id}/used) | yes | yes | yes | yes |
| Create, change and delete items; set limits and the rotation schedule | no | yes | yes | yes |
Set or change agent exposure (slug, agent_meta) and honeytoken | no | no | yes | yes |
Switch cloud use on or off (PUT /v1/vault/items/{id}/cloud); set or remove a deriver | no | no | no | yes |
A caller who is not a member of the vault gets 404, the same answer as for a vault that does not exist.
429 answers
Every 429 has the body { error: { message, type: 'rate_limit_error', code } } and a Retry-After header in seconds.
code | Routes | When | Retry-After |
|---|---|---|---|
vault_rate_limited | POST /v1/vault/use, /v1/vault/http, /v1/vault/resolve, /v1/vault/derive | a per-handle limit of the item is reached | 60 for the per-minute limit, 3600 for the daily limits |
rate_limit_exceeded | GET /v1/vault/hibp/range/{prefix} | more than 300 calls per user in one hour | 600 |
rate_limit_exceeded | POST /v1/vault/add-requests | 20 add requests are already pending | 60 |
Device keys and step-up
An approval proves who decided. Besides the v1 signature of your vault key, a decision can carry a v2 proof from a hardware-backed device key (Android Keystore on phone and watch, Windows Hello on desktop) or from a passkey. Device keys are public keys only; the private key never leaves the secure hardware of the device.
Enrolment
POST /v1/vault/devices/challengereturns a single-usenonce(32 random bytes, base64url) valid for 5 minutes.- The device creates its key and both your vault key and the device key sign the enrolment message:
codai-vault/device-enroll/v1|<device_id>|<kind>|<spki_sha256>|<nonce>, wherespki_sha256is base64url(SHA-256(SubjectPublicKeyInfo DER)). POST /v1/vault/devicessendsdevice_id(android-…,wear-…,desktop-…,ios-…orcli-…),kind(android_keystore,windows_hello,ios_secure_enclaveorcli_ed25519),name,surface(phone,wear,desktoporcli),public_key,nonce,vault_signature,device_signatureand, when available, anattestation. The answer is201 { device }.
Re-enrolling the same device_id replaces its key. GET /v1/vault/devices lists your keys (revoked ones included), PATCH /v1/vault/devices/{id} renames one and DELETE /v1/vault/devices/{id} revokes it at once: its next proof answers 403 device_revoked.
All binary values are base64url without padding. Android signs with ECDSA P-256 SHA-256 (DER); Windows Hello signs with RSASSA-PKCS1-v1_5 SHA-256; cli_ed25519 (the software key codaid vault enroll creates) signs with Ed25519 (raw 64-byte signature, public_key = Ed25519 SubjectPublicKeyInfo DER).
Attestation kinds
attestation | Meaning |
|---|---|
android-strongbox | Google key attestation verified, key held in a StrongBox secure element. |
android-tee | Google key attestation verified, key held in the TEE. |
tpm | A TPM attestation of the Windows Hello key verified against the Microsoft TPM roots. |
hello-unattested | A Windows Hello key without a TPM attestation. |
ios-unattested | An iPhone Secure Enclave P-256 key (ios_secure_enclave, surface phone, Face ID per signature). No attestation yet, so it approves T3 requests only; a T4 approve answers 403 step_up_required. |
software | A cli_ed25519 key kept in a file on the machine. It can only prove /v1/vault/resolve calls; it can never approve or unlock (403 device_not_allowed). |
Android keys must send attestation: { android_chain: [...] } (leaf first); the attestation challenge is the nonce bytes and the attested key must equal public_key. Windows Hello keys may send attestation: { tpm: { attestation, cert_chain } }; without it the key is hello-unattested, with an invalid one the enrolment fails. iPhone keys (ios_secure_enclave) send no attestation at all (sending one fails with attestation_invalid); the signature is ECDSA P-256 SHA-256 in DER, like Android. Errors: 400 invalid_nonce, possession_failed, attestation_required, attestation_invalid; 403 invalid_signature; 409 device_id_taken; 503 revocation_unavailable (Google's revocation list could not be checked, retry later).
Approval messages
- v1 (vault key):
codai-vault/approve/v1|<approval_id>|<action_hash>|<decision>|<scope>, sent assignature. - v2 (device key or passkey):
codai-vault/approve/v2|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>, sent asproof: { type: 'device', device_id, signature }. - v3 (device key or passkey + number matching):
codai-vault/approve/v3|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>|<preview_digest>|<match_number>, sent withmatch_numbernext toproof.preview_digestis base64url(SHA-256) of the canonical text ofrequest.preview(codai-vault/preview/v1, thentitle,agent,mode,host,action,purpose,taskaskey=valuelines, with\, newline and CR escaped), so the signature covers exactly what the approver saw. - passkey: the WebAuthn challenge is base64url(SHA-256(the v2 message with device id
passkey)), sent asproof: { type: 'passkey', response }.
Send at most one of signature and proof. step_up_acr is still accepted but ignored: a client-claimed step-up proves nothing.
Rules by tier
- Deny needs no proof (a notification Deny button is enough).
- T3 approve accepts a valid v1 signature, device proof or passkey proof.
- T4 approve requires a device or passkey proof; a v1 signature answers 403
step_up_required. Every attestation kind above qualifies today. - Wear is T3-only: a watch key cannot be attested as biometric-bound, so a proof from a
weardevice on a T4 approval answers 403wear_t3_only. - Number matching: every T3/T4 request gets a 2-digit
match_number, returned only to the agent (/vault/use,/vault/http,/vault/resolve). The approver seesrequest.previewand threerequest.match_choices, never the number. A T4 approve on such a request needsmatch_numberwith a v3 proof: none answers 403match_required; a wrong number denies the request at once and answers 403match_mismatch. T3 approvals and denies still accept v1/v2.
Other proof errors (403): invalid_signature, unknown_device, device_revoked, passkey_invalid.
Resolve proof (local brokers)
POST /v1/vault/resolve returns values to a local broker (codaid, desktop) that injects them into a child process and redacts its output. The broker must prove it holds an enrolled device key; the x-codai-agent header alone is not trusted.
POST /v1/vault/resolve/challengereturns a single-usechallengevalid for 120 s.- The broker signs
codai-vault/resolve/v1|<challenge>|<device_id>|<digest>with its device key, wheredigest= base64url(SHA-256(thehandlesjoined with\n, then\n\n, thenaction)), handles exactly as sent and in the same order. - The resolve body carries
proof: { device_id, challenge, signature }.
Errors: 400 proof_required (no proof), 400 invalid_challenge (unknown, used or expired), 403 invalid_device_proof (key not yours, revoked or bad signature), 403 broker_only (not a Kodwen broker). The audit event use.resolved records the device_id.
Anomaly guards
Every agent use is also checked against your anomaly guards, read from GET /v1/vault/account (guards) and changed with PATCH /v1/vault/settings { default_tier?, guards? } (send at least one; guards is partial and merged over the current values, the answer is the effective { default_tier, guards }).
| Guard | Default | Fires when | Reason |
|---|---|---|---|
new_host | true | no allowed use or grant for this item and host in the last 30 days | anomaly:new_host |
burst_max / burst_window_s | 5 / 60 | at least burst_max agent requests for the item within the window (burst_max: 0 = off) | anomaly:burst |
hour_start / hour_end / tz | 7 / 23 / Europe/Bucharest | the use falls outside [hour_start, hour_end) in tz (both hours null = off) | anomaly:hour |
An anomaly forces the decision to t4: the user approves with a device key or passkey (number matching included), and standing grants that were not approved at t4 are ignored for that use. A deny stays deny.
Panic lock and unlock
POST /v1/vault/lock (body optional, { via? }) is the panic button: it revokes every live grant, denies every pending approval and locks the vault, answering { locked_at, grants_revoked, approvals_denied }. Device keys stay enrolled so you can unlock. The call is idempotent. While locked, GET /v1/vault/account shows locked_at and lock_reason (panic or honeytoken) and every agent use is denied with reason vault_locked (/v1/vault/http and /v1/vault/resolve answer 403 vault_denied).
Unlocking needs a hardware-backed proof over a single-use challenge:
POST /v1/vault/unlock/challengereturns{ challenge, expires_at, message_template }(32 random bytes, base64url, valid 5 minutes).- Build the message
codai-vault/unlock/v1|<challenge>|<device_id|passkey>:- device key (phone, desktop): the key signs the UTF-8 message with your device id, sent as
proof: { type: 'device', device_id, signature }. Watch keys are refused (wear_t3_only). - passkey (hub): the WebAuthn challenge is base64url(SHA-256(the message with
passkeyin place of the device id)), sent asproof: { type: 'passkey', response }.
- device key (phone, desktop): the key signs the UTF-8 message with your device id, sent as
POST /v1/vault/unlock { challenge, proof, via? }answers{ unlocked: true }. Errors (403):challenge_invalid,unknown_device,device_revoked,wear_t3_only,invalid_signature,passkey_invalid.
The audit log records vault.locked, vault.unlocked, vault.unlock_failed and use.locked.
Honeytokens
An item marked honeytoken is a decoy no legitimate agent should touch. A use of it is denied and raises an alarm, and it now also locks the vault (lock_reason: 'honeytoken') and sends a data push to every active device: { kind: 'vault.alarm', reason: 'honeytoken', locked: '1', title?, agent?, host? } (a panic lock pushes the same alarm with reason: 'panic'). Unlock as above once you know what happened.
Per-handle limits
Cap how often agents may use one item. GET /v1/vault/limits lists { limits: [{ item_id, per_minute, per_day, units_per_day }] }; PUT /v1/vault/items/{id}/limits { per_minute, per_day, units_per_day } sets them (all three keys required, null removes one cap, all null removes the limits; editor or above on the item's vault).
Only allowed uses count. An http use costs 2 units, every other mode 1. Over a cap, /v1/vault/use, /v1/vault/http and /v1/vault/resolve answer 429 { error: { code: 'vault_rate_limited', type: 'rate_limit_error', message } } with Retry-After (60 s for the per-minute cap, 3600 s for the daily caps), and the audit log records use.limited.
Grants end with their task
A grant created for a task (scope task, or any grant carrying the task you sent on /v1/vault/use) is revoked when the task ends: on POST /v1/tasks/{id}/confirm, on DELETE /v1/sessions/{id}, and by a sweeper every 5 minutes for closed tasks. Agents and brokers can end them explicitly with POST /v1/vault/grants/revoke-task { task }, which answers { revoked: n }. GET /v1/vault/grants is unchanged; clients group its rows by task to show live leases.
Add requests
When an agent needs a credential the vault does not have, it must never ask you to paste it into the chat. It creates an add request instead, and you type the value on a Kodwen page where it is encrypted in your browser (or in the desktop app) before it is saved.
POST /v1/vault/add-requests { kind, title, slug?, hosts?, env_var?, fields?, purpose, task? }answers201 { id, url, status, expires_at }.urlishttps://hub.codai.ro/vault/add?req=<token>; the token appears only in this response.fieldsare field names to ask for (at most 10), never values. A request lives 10 minutes; at most 20 can be pending per user.GET /v1/vault/add-requests/{id}answers{ id, status, slug, handle, item_id, expires_at }, wherestatusispending,completed,cancelledorexpiredandhandleis{{vault:<slug>}}once completed.DELETE /v1/vault/add-requests/{id}cancels it.POST /v1/vault/add-requests/lookup { token }returns the request to the signed-in user it belongs to (desktop and phone deep links); anyone else gets 404.POST /v1/vault/add-requests/{id}/complete { item_id }marks it completed; the item must be in one of your vaults. Repeating the call with the same item is a no-op.
The desktop app opens codai://vault/add?req=<token> when it is installed; the phone opens the hub page in a Custom Tab. The audit log records add.requested, add.completed and add.cancelled.
From a terminal, codaid vault add --title <t> [--kind api_key] [--slug s] [--host h]… [--env-var NAME] [--field f]… [--purpose p] [--json] creates the same request and prints the link to open.
From MCP: URL elicitation
The vault MCP toolset (gateway and codaid vault mcp) has two tools for this: vault_add_secret (same inputs as the REST call) and vault_add_status { id }.
initializeanswers with anMcp-Session-Idheader that records whether your client declaredcapabilities.elicitation.url. The header is signed and stateless (v1.<payload>.<HMAC-SHA256>, valid 30 days); send it back on later calls.- With URL elicitation declared,
tools/call vault_add_secretanswers the JSON-RPC error-32042withdata.elicitations = [{ mode: 'url', elicitationId, url, message }], as defined by the MCP specification (2025-11-25). The client shows the link; after you add the secret, the agent callsvault_add_status(or callsvault_add_secretagain with the same slug or title within 10 minutes, which reuses the request) and receives the handle. - Fallback: without the capability (or without a session id), the tool returns a normal result with the link and the instruction "open it, then call vault_add_status".
Security
- The URL carries only a single-use 32-byte token; the server stores its SHA-256, never the token itself.
- The link is not pre-authenticated: the hub page requires sign-in and shows the request only to the user it was created for. Anyone else sees "not found".
- The page never receives anything secret from the server. It seals the item in the browser with your vault keys and saves it through the normal item path; the agent only ever sees the handle.
Emergency access
Name another Kodwen user who can read chosen vaults if you cannot, after a waiting period you control.
| Status | Meaning | Next |
|---|---|---|
invited | Invite sent by email (valid 14 days). | Contact accepts in their hub. |
accepted | Contact accepted; no keys shared yet. | Your client seals the vault keys to the contact. |
sealed | Vault keys sealed to the contact's X-Wing key. | Contact may request access. |
requested | Contact asked; release_at = now + delay_hours. | You deny or approve, or the delay passes. |
granted | Contact can read the chosen vaults. | You revoke. |
revoked | Ended by either side; sealed keys deleted. | — |
GET /v1/vault/emergencyanswers{ outgoing, incoming }.POST /v1/vault/emergency { email, delay_hours, vault_ids? }always answers202 { id }, with the same shape whether or not the address has a Kodwen account, so it cannot be used to discover accounts.delay_hoursis 24, 48 or 72;vault_idsdefaults to your personal vault. At most 5 contacts.POST /v1/vault/emergency/accept { token }(contact): needs a vault account (404vault_not_setup) and cannot be your own invite.PUT /v1/vault/emergency/{id}/seal { grantee_fingerprint, sealed_keys }(you):sealed_keysmust cover exactlyvault_ids. If the contact's key fingerprint is not the one you verified, the answer is 409fingerprint_changed. Compare the fingerprint shown on both devices before sealing.PATCH /v1/vault/emergency/{id} { delay_hours?, vault_ids? }(you): changing the vaults drops the sealed keys and returns the contact toaccepted.DELETE /v1/vault/emergency/{id}: you revoke or the contact declines.POST /v1/vault/emergency/{id}/request { reason? }(contact, statussealed), thenPOST /v1/vault/emergency/{id}/denyor/approve(you, statusrequested). A sweeper grants access oncerelease_athas passed.GET /v1/vault/emergency/{id}/vault-data(contact, statusgranted) returns the ciphertext of the chosen vaults and the sealed vault keys; the contact decrypts locally. Access is read-only and never extends to agents; every read is logged asemergency.accessedin your audit log.
Every transition notifies the other side: FCM data push { kind: 'vault.emergency', contact_id, event, other_name, release_at } on phone, Web Push in the hub and an email. The audit log records emergency.invited|accepted|sealed|requested|denied|approved|granted|revoked|accessed.
Rotation and breach watch
Breach check (HIBP)
GET /v1/vault/hibp/range/{prefix} proxies the Pwned Passwords range API with k-anonymity: the client hashes each password with SHA-1 locally, sends only the first 5 hex characters and matches the suffix in the answer itself. The gateway never sees a password or a full hash. Responses use Add-Padding: true so their size does not reveal the prefix, are cached 24 hours and are limited to 300 calls per user per hour (429 above that, 503 hibp_unavailable when the upstream fails). The client then reports POST /v1/vault/rotation/breach-report { results: [{ item_id, count }] } (at most 500); a newly breached item sends a vault.breach push.
Rotation queue
GET /v1/vault/rotationlists{ items: [{ item_id, slug, title, kind, interval_days, rotated_at, due_at, overdue, breached, breach_count, breach_checked_at, snoozed_until, provider, create_url }] }, breached first, then overdue, then soonest due.PUT /v1/vault/items/{id}/rotation { interval_days?, provider?, snoozed_until? }sets the schedule (1–730 days) and the provider;POST /v1/vault/items/{id}/rotatedrecords a rotation and clears the breach flag.- Defaults: 90 days for
api_key,oauth_tokenandenvitems; logins are watched for breaches only; items with a deriver are skipped (their credentials are short-lived). - A sweeper every 6 hours sends one
vault.rotationpush per overdue item per cycle (not while snoozed). - Keys that cannot be rotated through an API stay static, and the list links to the page where you create a new one:
provider | create_url |
|---|---|
stripe (restricted keys) | https://dashboard.stripe.com/apikeys |
github_pat (fine-grained PAT) | https://github.com/settings/personal-access-tokens/new |
openai | https://platform.openai.com/api-keys |
google_api_key | https://console.cloud.google.com/apis/credentials |
JIT credentials
A deriver turns a root credential into a short-lived, scoped one each time it is used, so the long-lived secret never reaches the command.
GET /v1/vault/derivers,PUT /v1/vault/items/{id}/deriver { provider, config, max_ttl_s }(300–43200 s),DELETE /v1/vault/items/{id}/deriver.configis non-secret.
provider | config | Root item fields | Derived fields → env |
|---|---|---|---|
aws_sts | role_arn, region (default us-east-1), session_policy? (≤ 2048 chars), external_id?, duration_s 900–43200 | access_key_id, secret_access_key | access_key_id, secret_access_key, session_token → AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN |
gcp_sa | mode key | impersonate, target_service_account?, scopes (default cloud-platform), lifetime_s 300–3600 | json (service-account key) | token → CLOUDSDK_AUTH_ACCESS_TOKEN, GOOGLE_OAUTH_ACCESS_TOKEN |
cloudflare | account_id?, policies, ttl_s 300–86400 | token (allowed to create tokens) | token → CLOUDFLARE_API_TOKEN |
github_app | app_id, installation_id, repositories?, permissions? | private_key (PEM) | token → GH_TOKEN, GITHUB_TOKEN |
{{vault:slug}} on a derived item is the first derived field; {{vault:slug.<field>}} selects one.
POST /v1/vault/derive is for Kodwen brokers only (codaid, desktop): body { handle, action, purpose?, provenance?, task?, ttl_s?, proof }, with the same device proof as /v1/vault/resolve over handles = [handle]. The proof is checked before any policy call; without it the answer is 400 proof_required, a non-broker caller gets 403 broker_only. After the usual policy (env mode, approvals included) the item must be cloud-usable (409 not_cloud_usable) and have a deriver (409 no_deriver). The answer is { provider, values, env, expires_at } with Cache-Control: no-store; an upstream failure answers 502 derive_failed with the provider message scrubbed of anything token-shaped. The audit log records derive.issued { provider, expires_at, device_id }, never the value.
Items that are not cloud-usable derive in the local broker instead (codaid vault run, desktop), with the same providers; the broker injects the env names into the child and redacts every derived value from its output.
/v1/vault/http on an item with a gcp_sa, cloudflare or github_app deriver injects the derived bearer token instead of the root secret. The token is cached in gateway memory until 60 seconds before it expires.
Agent autofill
The desktop agent browser can sign in to sites without the model seeing the password. In browser_type, write {{vault:slug.field}} (username, password, totp or any field). The desktop app then:
- reads the page origin over CDP and checks the item's bound hosts (
agent_meta.hosts); an unbound host fails withvault_host_mismatch; - asks for a policy decision with mode
autofillandhost= the page host (approval on your device when the tier requires it); - decrypts locally, checks that the page is still on the same origin, and injects the value; the tool answers
{ ok, filled: true }without the value.
Snapshots never show the value of a password input, and browser_eval / click output is redacted against every value filled in that browser session ([vault:<slug>]).
The browser extension also accepts agent_fill requests from the desktop app over native messaging and fills only when the active tab is on exactly the requested origin.
Browser extension and Windows passkeys
Kodwen Vault for Chrome and Edge (Manifest V3, distributed unlisted on the Chrome Web Store and hidden on Edge Add-ons):
- Works on its own: the vault crypto is bundled as WASM (no remote code). Unlock with your password and Secret Key; the unlocked session lives only in the service worker's memory and locks after 10 minutes idle by default.
- Fills only on the origin bound to the login: the service worker checks the tab origin, and the content script checks
location.originagain before writing a field. - Optional native messaging to the desktop app (host
ro.codai.vault): unlock with Windows Hello on desktop, fill from the desktop vault (candidates,fill) and agent fills. The host manifest'sallowed_originslists only the store extension ids, and the desktop app checks the extension id and the page origin on every request again. - Permissions:
storage,scripting,activeTab,contextMenus,alarms,nativeMessaging,idle; access to all sites is optional and asked at runtime for inline fill. Gateway calls usevia: 'extension'.
Windows 11 passkey provider: the Microsoft Store (MSIX) build of the desktop app registers as a passkey provider, so Windows can create and use passkeys stored as passkey items in your vault, after Windows Hello. Every request from Windows is checked against the operation signing key Windows registered for the plugin. NSIS installs do not register it and show "available in the Microsoft Store version".
Android: passwords and passkeys for other apps
The Kodwen Android app downloaded from GitHub (not the Play build) is a Credential Manager provider on Android 14+ and an Autofill service on Android 11–13. Enable it in Settings › Passwords & accounts (or Autofill service).
- Fill: matching logins (
androidapp://<package>or the site's domain and subdomains) are offered while the vault is unlocked; each fill asks for a biometric pass. - Save: when you sign up or change a password in another app or a browser, the phone offers to save it to your vault: the first personal vault by default, or any vault where you are owner, manager or editor. If a login with the same username and address exists, it offers to update it. The item is sealed on the phone and uploaded as ciphertext (
PUT /v1/vault/items/:id); it is never agent-visible (no handle). - Passkeys: sites and apps can create and use passkeys (ES256) stored as
passkeyitems in your vault, the same format the desktop Windows provider uses, so a passkey created on the phone also works on desktop. The private key stays inside the vault item; signing happens in the vault core on the device after a biometric pass, and the signature counter is saved back to the item. Browser requests are accepted only from browsers on Google's privileged browser list and only when the relying party id matches the page origin. - The CTAP / WebAuthn core (authenticator data, COSE key,
noneattestation, assertion) is shared by the desktop provider and the phone (crates/codai-vault, featurepasskey).
Org vaults
An organisation owner or admin creates a shared vault with POST /v1/vault/vaults { id, encrypted_meta, sealed_key, org_id } (kind: shared). GET /v1/vault/vaults then shows, per vault, org_id, your role (owner, manager, editor, viewer), quorum, rotation_needed_at and key_epoch. The server never sees a vault key: every member gets their own copy sealed to their X-Wing public key by an owner or manager's client.
GET /v1/vault/vaults/{id}/members(any member):{ members: [{ user_id, email, name, role, fingerprint, key_epoch, added_at }] }.GET /v1/vault/vaults/{id}/candidates(owner/manager): organisation members with a vault account who are not in the vault yet, withpublic_keysandfingerprint.PUT /v1/vault/vaults/{id}/members/{userId} { role, sealed_key, fingerprint }(owner/manager): show the candidate's fingerprint, have it confirmed (out of band, as for emergency contacts), seal the vault key topublic_keysand upload it. If the member's keys changed since the fingerprint was read the answer is 409fingerprint_changed; a user outside the organisation answers 403. The owner row cannot be changed.PATCH /v1/vault/vaults/{id}/members/{userId} { role }changes the role (manager,editororviewer).DELETE /v1/vault/vaults/{id}/members/{userId}(owner/manager, or the member leaving) deletes the row at once and setsrotation_needed_at. Removing someone from the organisation does the same on every vault of that organisation.
Key rotation
A removed member may still hold an old copy of the vault key, so after any removal the vault shows a banner until an owner or manager rotates it. The client generates a new vault key, re-seals it to every remaining member, re-wraps every item key, and sends everything in one call:
POST /v1/vault/vaults/{id}/rotate { key_epoch, encrypted_meta, members: [{ user_id, sealed_key }], items: [{ id, revision, wrapped_key, ciphertext }] }
key_epoch is the current epoch + 1 and every item revision is its current revision + 1. The request must cover every current member and every live item (at most 2000): otherwise the answer is 409 rotation_incomplete with the missing ids and nothing changes. It runs in one transaction and clears rotation_needed_at.
Quorum approvals
On an org vault the owner can require several people to approve sensitive uses: PUT /v1/vault/vaults/{id}/quorum { m, scope } with m from 2 to 9 and scope critical (critical items only) or high (high and critical); { m: null } turns it off. m cannot exceed the number of owner/manager members (400).
When an agent asks for a t3 or t4 use of a matching item, the approval is created with quorum_m = m and a 24-hour expiry. If fewer than m eligible approvers exist (owners and managers other than the requester, with a vault account), the use is denied at once with reason quorum_unavailable. Every eligible approver gets a push, and GET /v1/vault/approvals lists the request with quorum: { m, approvals, needed, voted }.
- Vote with
POST /v1/vault/approvals/{id}/vote { decision, proof }. Each vote carries its own proof over approve/v4:codai-vault/approve/v4|<approval_id>|<action_hash>|<decision>|once|<approver_user_id>|<device_id>|<preview_digest>signed by one of the approver's own device keys (proof: { type: 'device', device_id, signature }), or as a passkey whose WebAuthn challenge is base64url(SHA-256(the message with device idpasskey)). The approver's user id is part of the message, so one approver's proof cannot be replayed as another's. Software keys are refused; t4 needs an attested device or a passkey. There is no match number: approvers are not next to the agent. - Any deny ends the request (
denied). Them-th distinct approve marks itapprovedand creates a singleoncegrant for the requester; the audit eventapproval.approvedlists every voter's device. Each vote is logged asapproval.vote. - Errors: 409
already_voted, 409not_pending, 403 for a bad proof or an approver who is not eligible.POST /v1/vault/approvals/{id}/decideon a quorum approval answers 409quorum_required.
What the approver signs (preview v2)
The approval preview now includes the agent's delegation chain (who asked whom, outermost first, joined with >, cut at 300 characters). The canonical text is codai-vault/preview/v2 followed by title, agent, mode, host, action, purpose, task, chain as key=value lines, with the same escaping as v1. A preview object whose chain is a string uses v2; otherwise v1. Hub, desktop and phone show the chain next to the request, and every use event stores it.
Transition: until the next desktop and phone release, the gateway also stores the v1 digest of the same preview (without chain). A v3 proof is checked against the v2 digest first and then against the v1 digest; a v4 proof must use the v2 digest.
Offline approvals by QR
When your phone has no network, the desktop can carry the approval both ways with QR codes:
- The desktop shows a request QR (compact JSON, UTF-8, error correction M):
{ "v": "codai-vault-qr/1", "t": "req", "id", "h": "<action_hash>", "tier": "t3|t4", "u": "<requester user id>", "p": { ...preview... }, "c": ["47","12","83"] }. - The phone refuses a QR whose
uis not the signed-in user, recomputes the preview digest frompitself (it never trusts a digest from the QR), showspand asks you to tap the number the agent shows you (one ofc; the number itself is not in the QR). - The phone signs approve/v3 (v2 when there is no number) with its device key and shows a response QR:
{ "v": "codai-vault-qr/1", "t": "res", "id", "d": "approve|deny", "s": "once|task|1h|8h", "dev": "<device id>", "m": "<number or empty>", "sig": "<base64url>" }. - The desktop reads it with the webcam and posts
POST /v1/vault/approvals/{id}/decide { decision, scope, via: 'qr', match_number, proof: { type: 'device', device_id, signature } }. The gateway verifies the device proof exactly as if the phone had posted it.
Policy simulator
Before you change your rules, see what they would have done. POST /v1/vault/policy/simulate { rules?, default_tier?, since?, limit? } replays your past agent uses under a draft: rules replaces your current rules, default_tier your default tier; since defaults to 30 days ago and limit to 200 (at most 500). It is read-only and answers:
{ evaluated, changed, grants: 'none', rows: [{ event_id, at, item_id, title, agent, chain, mode, host, current_tier, simulated_tier, reasons, source }] }
The sources are use.allowed, use.denied and approval.requested events (they store chain, mode, host, action_hash, purpose, task and provenance from this release on, never the raw action) plus the stored requests of older t3/t4 approvals (source: 'approval'). Sensitivity and bound hosts come from each item's current metadata. Grants and anomaly guards are not replayed. The same check is available to agents as the read-only MCP tool vault_policy_simulate.
Signed receipts
Security-relevant events are signed: use.allowed, use.denied, approval.approved, approval.denied, approval.vote and honeytoken. Each carries a DSSE envelope at payload.receipt in the audit log:
payloadType:application/vnd.codai.vault-receipt+json;- payload:
{ v: 1, event, user_id, item_id, agent, tier, action_hash, reasons, device_id, approval_id, at }(metadata only, never a value); signatures: [{ keyid, sig }], ECDSA P-256 SHA-256 from a Cloud KMS key.
To verify, fetch GET /v1/vault/receipts/keys ({ keys: [{ kid, alg: 'ES256', pem }] }, cached 1 hour), pick the key whose kid equals keyid, and check the signature over the DSSE pre-authentication encoding DSSEv1 <len(type)> <type> <len(payload)> <payload>. The hub does this in the browser and shows a "signed" badge. If signing is unavailable the event is still stored, unsigned; verification never passes an unsigned or tampered receipt.
Weekly digest
Every Monday at 07:10 (Europe/Bucharest) users with vault activity that week get an email through Kodwen mail: uses, denials, approvals, new devices, items due for rotation. Turn it off with the unsubscribe link, the hub setting or PATCH /v1/vault/settings { digest_opt_out: true }; GET /v1/vault/account shows digest_opt_out.
Scoped API keys
An API key can carry scopes; null (every existing key) is a full key. A scoped key may call only:
| Scope | Routes |
|---|---|
vault.read | GET /v1/vault/account, GET /v1/vault/vaults, GET /v1/vault/items, GET /v1/vault/catalog, GET /v1/vault/receipts/keys |
vault.add | POST /v1/vault/add-requests, GET /v1/vault/add-requests/{id}, DELETE /v1/vault/add-requests/{id} |
Anything else answers 403 key_scope_forbidden. Connect browser extension on the hub keys page mints a key with vault.read + vault.add, valid 180 days, after a fresh sign-in, and shows it once. The extension stores it encrypted with a non-extractable AES-GCM key kept in IndexedDB, never in plaintext. GET /v1/vault/account returns key_scopes for the calling key, and the extension warns when it is running on a full key.
Repo honeytokens
codaid vault import --apply also plants 2 decoy items per imported repo (named after --repo or the folder): agent-visible api_key items marked honeytoken, such as aws-prod-legacy, stripe-live-old, github-deploy-old or openai-prod-old, bound to the real provider host and filled with random values in the provider's format (never real). The choice is deterministic per repo and skips slugs you already have; a dry run lists them as decoy. Repo files are never touched. Any use triggers the honeytoken path above (deny, lock, vault.alarm push). Pass --no-honeytokens to skip them.
bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
bundle*Account bundle: public keys, private keys sealed under the account key, unlock slots (wrapped keys + KDF parameters). No plaintext.
fingerprint*stringPublic key fingerprint to compare between devices.
default_tier*string"t1""t2""t3""t4"revision*integerlocked_at?stringWhen the vault was locked (panic or honeytoken); null when unlocked. While locked every agent use is denied with reason vault_locked.
date-timelock_reason?stringWhy the vault is locked.
"panic""honeytoken"guards?key_scopes?array<>Scopes of the API key that made this call; null = full (unscoped) key. The browser extension warns when it runs on a full key.
digest_opt_out?booleanTrue when you turned off the weekly vault digest email.
curl -X GET 'https://ai.codai.ro/v1/vault/account'{ "bundle": {}, "fingerprint": "string", "default_tier": "t1", "revision": 0, "locked_at": "2019-08-24T14:15:22Z", "lock_reason": "panic", "guards": { "new_host": true, "burst_max": 0, "burst_window_s": 10, "hour_start": 0, "hour_end": 1, "tz": "string" }, "key_scopes": [ "vault.read" ], "digest_opt_out": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
bundle*revision?integerUpdated.
application/json- response
[key: string]?anycurl -X PUT 'https://ai.codai.ro/v1/vault/account' \ -H 'Content-Type: application/json' \ -d '{ "bundle": {}}'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
default_tier?string"t1""t2""t3""t4"guards?Any subset of VaultGuards; hour_start and hour_end are both null (off) or both set.
digest_opt_out?booleanTrue stops the weekly digest email (Monday 07:10 Europe/Bucharest).
Effective settings.
application/json- response
default_tier*string"t1""t2""t3""t4"guards*digest_opt_out?booleancurl -X PATCH 'https://ai.codai.ro/v1/vault/settings' \ -H 'Content-Type: application/json' \ -d '{}'{ "default_tier": "t1", "guards": { "new_host": true, "burst_max": 0, "burst_window_s": 10, "hour_start": 0, "hour_end": 1, "tz": "string" }, "digest_opt_out": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
via?string"hub""desktop""phone""wear""extension"Locked.
application/json- response
locked_at*stringdate-timegrants_revoked*integerLive grants revoked by this lock.
approvals_denied*integerPending approvals denied by this lock.
curl -X POST 'https://ai.codai.ro/v1/vault/lock' \ -H 'Content-Type: application/json' \ -d '{}'{ "locked_at": "2019-08-24T14:15:22Z", "grants_revoked": 0, "approvals_denied": 0}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
challenge*string32 random bytes, base64url. Single use.
expires_at*string5 minutes after issue.
date-timemessage_template*stringcodai-vault/unlock/v1|<challenge>|<device_id or passkey>: the UTF-8 message the proof covers.
curl -X POST 'https://ai.codai.ro/v1/vault/unlock/challenge'{ "challenge": "string", "expires_at": "2019-08-24T14:15:22Z", "message_template": "string"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
challenge*stringChallenge from POST /v1/vault/unlock/challenge.
proof*|Proven step-up for a decision. device: an enrolled device key signs codai-vault/approve/v2|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>. passkey: a WebAuthn assertion whose challenge is base64url(SHA-256(the same message with device id passkey)). With match_number the message is v3: codai-vault/approve/v3|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>|<preview_digest>|<match_number>, where preview_digest = base64url(SHA-256(the canonical text of request.preview)).
via?string"hub""desktop""phone""wear""extension"Unlocked.
application/json- response
unlocked*booleancurl -X POST 'https://ai.codai.ro/v1/vault/unlock' \ -H 'Content-Type: application/json' \ -d '{ "challenge": "string", "proof": { "type": "device", "device_id": "string", "signature": "string" }}'{ "unlocked": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
limits*array<>curl -X GET 'https://ai.codai.ro/v1/vault/limits'{ "limits": [ { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "per_minute": 1, "per_day": 1, "units_per_day": 1 } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
per_minute*integer1 <= value <= 10000per_day*integer1 <= value <= 1000000units_per_day*integer1 <= value <= 1000000OK; limits is null when removed.
application/json- response
limits*curl -X PUT 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/limits' \ -H 'Content-Type: application/json' \ -d '{ "per_minute": 1, "per_day": 1, "units_per_day": 1}'{ "limits": { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "per_minute": 1, "per_day": 1, "units_per_day": 1 }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
vaults*array<>curl -X GET 'https://ai.codai.ro/v1/vault/vaults'{ "vaults": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "kind": "personal", "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d", "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b", "encrypted_meta": "string", "role": "owner", "sealed_key": {}, "quorum": { "m": 2, "scope": "critical" }, "rotation_needed_at": "2019-08-24T14:15:22Z", "key_epoch": 1, "updated_at": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
id*stringuuidencrypted_meta*stringsealed_key*org_id?stringCreate an org vault in this organisation.
uuidCreated.
application/json- response
[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/vaults' \ -H 'Content-Type: application/json' \ -d '{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "encrypted_meta": "string", "sealed_key": {}}'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
since?stringOnly changes after this time. Send an ISO 8601 date-time with a time zone, for example 2026-11-10T12:00:00Z.
date-timeOK.
application/json- response
items*array<>server_time*stringThe since of your next call (see the operation description).
date-timehas_more*booleanTrue: call again now with since = server_time.
curl -X GET 'https://ai.codai.ro/v1/vault/items'{ "items": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de", "revision": 1, "wrapped_key": "string", "ciphertext": "string", "kind": "login", "slug": "string", "agent_meta": {}, "honeytoken": true, "cloud_usable": true, "deleted_at": "2019-08-24T14:15:22Z", "last_used_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z" } ], "server_time": "2019-08-24T14:15:22Z", "has_more": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
vault_id*stringuuidrevision*integer1 <= valuewrapped_key*stringPer-item key wrapped under the vault key (v1.<nonce>.<ciphertext>).
ciphertext*stringThe item sealed under its item key.
kind*string"login""api_key""env""ssh_key""oauth_token""note""card""identity""passkey"slug?stringHandle slug when the item is exposed to agents.
agent_meta?Public catalogue entry for agents (title, sensitivity, hosts, env var). Never secret values.
honeytoken?booleanDecoy flag. Owner or manager only; an update must send the stored value back.
Updated.
application/json- response
id*stringuuidrevision*integer1 <= valuecurl -X PUT 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Content-Type: application/json' \ -d '{ "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de", "revision": 1, "wrapped_key": "string", "ciphertext": "string", "kind": "login"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "revision": 1}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidDeleted.
application/json- response
deleted*booleancurl -X DELETE 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "deleted": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
item*OK.
application/json- response
cloud_usable*booleancurl -X PUT 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/cloud' \ -H 'Content-Type: application/json' \ -d '{ "item": {}}'{ "cloud_usable": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
rules*array<>curl -X GET 'https://ai.codai.ro/v1/vault/rules'{ "rules": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b", "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "agent": "string", "mode": "env", "tier": "t0", "createdAt": "2019-08-24T14:15:22Z", "updatedAt": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
item_id?stringuuidagent?stringmode?string"env""http""autofill""ssh_sign""reveal"tier*string"t0""t1""t2""t3""t4""deny"Created.
application/json- response
rule*The stored row as it is: the keys of this object are camelCase.
curl -X POST 'https://ai.codai.ro/v1/vault/rules' \ -H 'Content-Type: application/json' \ -d '{ "tier": "t0"}'{ "rule": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b", "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "agent": "string", "mode": "env", "tier": "t0", "createdAt": "2019-08-24T14:15:22Z", "updatedAt": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidDeleted.
application/json- response
[key: string]?anycurl -X DELETE 'https://ai.codai.ro/v1/vault/rules/497f6eca-6276-4993-bfeb-53cbbbba6f08'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
grants*array<>curl -X GET 'https://ai.codai.ro/v1/vault/grants'{ "grants": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b", "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "agent": "string", "mode": "env", "host": "string", "task": "string", "usesLeft": 0, "expiresAt": "2019-08-24T14:15:22Z", "revokedAt": "2019-08-24T14:15:22Z", "approvalId": "23bbe807-dea1-4601-b208-07fd1aaad2b6", "createdAt": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
task*string1 <= length <= 120OK.
application/json- response
revoked*integercurl -X POST 'https://ai.codai.ro/v1/vault/grants/revoke-task' \ -H 'Content-Type: application/json' \ -d '{ "task": "string"}'{ "revoked": 0}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidRevoked.
application/json- response
[key: string]?anycurl -X DELETE 'https://ai.codai.ro/v1/vault/grants/497f6eca-6276-4993-bfeb-53cbbbba6f08'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
limit?integer1-500, default 100.
OK.
application/json- response
events*array<>curl -X GET 'https://ai.codai.ro/v1/vault/events'{ "events": [ { "id": 0, "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b", "kind": "string", "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "agent": "string", "tier": "string", "payload": {}, "createdAt": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
approvals*array<>curl -X GET 'https://ai.codai.ro/v1/vault/approvals'{ "approvals": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "quorum": { "m": 2, "approvals": 0, "needed": 0, "voted": true }, "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b", "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "agent": "string", "tier": "t3", "request": {}, "actionHash": "string", "reasons": [ "string" ], "status": "pending", "expiresAt": "2019-08-24T14:15:22Z", "createdAt": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
id*stringuuidstatus*string"pending""approved""denied""expired""cancelled"tier*string"t3""t4"expiresAt*stringdate-timegrantScope?stringScope picked on approval: once, task, 1h or 8h.
decidedDeviceId?stringDevice key, or passkey, that proved the decision.
quorum?vault_id?stringVault of the item; present on quorum approvals.
uuid[key: string]?anycurl -X GET 'https://ai.codai.ro/v1/vault/approvals/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "pending", "tier": "t3", "expiresAt": "2019-08-24T14:15:22Z", "grantScope": "string", "decidedDeviceId": "string", "quorum": { "m": 2, "approvals": 0, "needed": 0, "voted": true }, "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
decision*string"approve""deny"scope*string"once""task""1h""8h"signature?stringv1 Ed25519 signature of your vault key, base64url.
proof?|Proven step-up for a decision. device: an enrolled device key signs codai-vault/approve/v2|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>. passkey: a WebAuthn assertion whose challenge is base64url(SHA-256(the same message with device id passkey)). With match_number the message is v3: codai-vault/approve/v3|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>|<preview_digest>|<match_number>, where preview_digest = base64url(SHA-256(the canonical text of request.preview)).
step_up_acr?stringDeprecatedIgnored: a client-claimed step-up proves nothing. Use proof.
via?stringqr: an offline phone signed the decision and the desktop read the response QR.
"hub""desktop""phone""wear""extension""qr"match_number?stringThe number the user tapped (one of request.match_choices); requires a device or passkey proof over the v3 message.
^[0-9]{2}$OK.
application/json- response
[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/approvals/497f6eca-6276-4993-bfeb-53cbbbba6f08/decide' \ -H 'Content-Type: application/json' \ -d '{ "decision": "approve", "scope": "once"}'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
nonce*string32 random bytes, base64url. Single use.
expires_at*string5 minutes after issue.
date-timecurl -X POST 'https://ai.codai.ro/v1/vault/devices/challenge'{ "nonce": "string", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
devices*array<>curl -X GET 'https://ai.codai.ro/v1/vault/devices'{ "devices": [ { "id": "string", "kind": "android_keystore", "name": "string", "surface": "phone", "public_key": "string", "attestation": "android-strongbox", "attestation_info": {}, "created_at": "2019-08-24T14:15:22Z", "last_used_at": "2019-08-24T14:15:22Z", "revoked_at": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
device_id*string^(android|wear|desktop|ios|cli)-[A-Za-z0-9._-]{4,80}$kind*string"android_keystore""windows_hello""cli_ed25519""ios_secure_enclave"name*string1 <= length <= 80surface*string"phone""wear""desktop""cli"public_key*stringSubjectPublicKeyInfo DER of the device key, base64url.
nonce*stringNonce from POST /v1/vault/devices/challenge.
vault_signature*stringEd25519 signature of your vault key over codai-vault/device-enroll/v1|<device_id>|<kind>|<spki_sha256>|<nonce>.
device_signature*stringThe device key over the same message (ECDSA P-256 DER on Android and for ios_secure_enclave, RSASSA-PKCS1-v1_5 SHA-256 for Windows Hello, raw 64-byte Ed25519 for cli_ed25519), base64url.
attestation?|Required for android_keystore (certificate chain, leaf first, attestation challenge = nonce bytes). Optional for windows_hello: a TPM attestation gives tpm, none gives hello-unattested. Must be absent for cli_ed25519 (surface cli, stored as software) and for ios_secure_enclave (surface phone, stored as ios-unattested).
Enrolled.
application/json- response
device*curl -X POST 'https://ai.codai.ro/v1/vault/devices' \ -H 'Content-Type: application/json' \ -d '{ "device_id": "string", "kind": "android_keystore", "name": "string", "surface": "phone", "public_key": "string", "nonce": "string", "vault_signature": "string", "device_signature": "string"}'{ "device": { "id": "string", "kind": "android_keystore", "name": "string", "surface": "phone", "public_key": "string", "attestation": "android-strongbox", "attestation_info": {}, "created_at": "2019-08-24T14:15:22Z", "last_used_at": "2019-08-24T14:15:22Z", "revoked_at": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringDevice id.
^(android|wear|desktop|ios|cli)-[A-Za-z0-9._-]{4,80}$application/json- body
name*string1 <= length <= 80OK.
application/json- response
device*curl -X PATCH 'https://ai.codai.ro/v1/vault/devices/string' \ -H 'Content-Type: application/json' \ -d '{ "name": "string"}'{ "device": { "id": "string", "kind": "android_keystore", "name": "string", "surface": "phone", "public_key": "string", "attestation": "android-strongbox", "attestation_info": {}, "created_at": "2019-08-24T14:15:22Z", "last_used_at": "2019-08-24T14:15:22Z", "revoked_at": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringDevice id.
^(android|wear|desktop|ios|cli)-[A-Za-z0-9._-]{4,80}$Revoked.
application/json- response
ok*booleancurl -X DELETE 'https://ai.codai.ro/v1/vault/devices/string'{ "ok": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
[key: string]?anycurl -X GET 'https://ai.codai.ro/v1/vault/catalog'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
handle*stringmode*string"env""http""autofill""ssh_sign""reveal"host?stringaction*stringpurpose?stringprovenance?string"user""agent""untrusted"chain?array<string>task?stringDecision.
application/json- response
tier*string"t0""t1""t2""t3""t4""deny"allowed*booleanreasons*array<string>Why this tier. Guard reasons: vault_locked (the vault is locked, tier deny), anomaly:new_host, anomaly:burst, anomaly:hour (tier forced to t4; grants not approved at t4 are ignored).
approval_id?stringuuidmatch_number?stringTwo digits to show the user with the approval request; they tap the same number on the approving device (number matching). Null when no approval is needed.
^[0-9]{2}$action_hash?stringHash of the exact request the approver signs.
cloud_usable?booleancurl -X POST 'https://ai.codai.ro/v1/vault/use' \ -H 'Content-Type: application/json' \ -d '{ "handle": "string", "mode": "env", "action": "string"}'{ "tier": "t0", "allowed": true, "reasons": [ "string" ], "approval_id": "15af2260-72db-4fc4-b587-6b841d6a3d55", "match_number": "string", "action_hash": "string", "cloud_usable": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
handle*stringurl*stringurimethod?string"GET""POST""PUT""PATCH""DELETE""HEAD"headers?body?stringpurpose?stringprovenance?string"user""agent""untrusted"chain?array<string>task?stringUpstream response (redacted).
application/json- response
[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/http' \ -H 'Content-Type: application/json' \ -d '{ "handle": "string", "url": "http://example.com"}'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
challenge*string32 random bytes, base64url. Single use; sign it into the next POST /v1/vault/resolve proof.
expires_at*string120 seconds after issue.
date-timecurl -X POST 'https://ai.codai.ro/v1/vault/resolve/challenge'{ "challenge": "string", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
handles*array<string>action*stringpurpose?stringprovenance?string"user""agent""untrusted"chain?array<string>task?stringproof*Decrypted items keyed by handle.
application/json- response
[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/resolve' \ -H 'Content-Type: application/json' \ -d '{ "handles": [ "string" ], "action": "string", "proof": { "device_id": "string", "challenge": "string", "signature": "string" }}'{}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
kind*string"login""api_key""env""ssh_key""oauth_token""note""card""identity""passkey"title*string1 <= length <= 120slug?stringSuggested handle slug (the user may change it).
hosts?array<>Hosts the secret should be bound to.
items <= 10env_var?string^[A-Z_][A-Z0-9_]{0,63}$fields?array<>Field names the user should fill in.
items <= 10purpose*stringWhy the agent needs this secret; shown to the user.
1 <= length <= 500task?string1 <= length <= 120Created or reused.
application/json- response
id*stringuuidurl*stringHub link the user opens to add the secret. Null when an equivalent pending request was reused (the link token is not stored).
status*stringA pending request past its expiry reads as expired.
"pending""completed""cancelled""expired"expires_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/add-requests' \ -H 'Content-Type: application/json' \ -d '{ "kind": "login", "title": "string", "purpose": "string"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "url": "string", "status": "pending", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
token*string^[A-Za-z0-9_-]{43}$OK.
application/json- response
id*stringuuidstatus*stringA pending request past its expiry reads as expired.
"pending""completed""cancelled""expired"agent*stringkind*string"login""api_key""env""ssh_key""oauth_token""note""card""identity""passkey"title*stringslug*stringhosts*array<string>env_var*stringfields*array<string>purpose*stringtask*stringhandle*stringitem_id*stringuuidexpires_at*stringdate-timecompleted_at*stringdate-timecreated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/add-requests/lookup' \ -H 'Content-Type: application/json' \ -d '{ "token": "string"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "pending", "agent": "string", "kind": "login", "title": "string", "slug": "string", "hosts": [ "string" ], "env_var": "string", "fields": [ "string" ], "purpose": "string", "task": "string", "handle": "string", "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "expires_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
id*stringuuidstatus*stringA pending request past its expiry reads as expired.
"pending""completed""cancelled""expired"slug*stringhandle*string{{vault:<slug>}} once completed.
item_id*stringuuidexpires_at*stringdate-timecurl -X GET 'https://ai.codai.ro/v1/vault/add-requests/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "pending", "slug": "string", "handle": "string", "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidCancelled.
application/json- response
id*stringuuidstatus*stringA pending request past its expiry reads as expired.
"pending""completed""cancelled""expired"slug*stringhandle*string{{vault:<slug>}} once completed.
item_id*stringuuidexpires_at*stringdate-timecurl -X DELETE 'https://ai.codai.ro/v1/vault/add-requests/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "pending", "slug": "string", "handle": "string", "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
item_id*stringuuidCompleted.
application/json- response
id*stringuuidstatus*stringA pending request past its expiry reads as expired.
"pending""completed""cancelled""expired"slug*stringhandle*string{{vault:<slug>}} once completed.
item_id*stringuuidexpires_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/add-requests/497f6eca-6276-4993-bfeb-53cbbbba6f08/complete' \ -H 'Content-Type: application/json' \ -d '{ "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "pending", "slug": "string", "handle": "string", "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
outgoing*array<>incoming*array<>curl -X GET 'https://ai.codai.ro/v1/vault/emergency'{ "outgoing": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z" } ], "incoming": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
email*stringemaillength <= 320delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids?array<>Vaults covered; omitted = every vault you own.
1 <= items <= 20Invited.
application/json- response
id*stringuuidcurl -X POST 'https://ai.codai.ro/v1/vault/emergency' \ -H 'Content-Type: application/json' \ -d '{ "email": "user@example.com", "delay_hours": 24}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
token*string^[A-Za-z0-9_-]{43}$Accepted.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/emergency/accept' \ -H 'Content-Type: application/json' \ -d '{ "token": "string"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
grantee_fingerprint*string8 <= length <= 200sealed_keys*Vault id → vault key sealed to the contact.
1 <= properties <= 20Sealed.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X PUT 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08/seal' \ -H 'Content-Type: application/json' \ -d '{ "grantee_fingerprint": "stringst", "sealed_keys": { "property1": { "alg": "xwing-hkdf-xchacha20poly1305", "ct": "string", "wrapped": "string" }, "property2": { "alg": "xwing-hkdf-xchacha20poly1305", "ct": "string", "wrapped": "string" } }}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
delay_hours?integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids?array<>Vaults covered; omitted = every vault you own.
1 <= items <= 20Updated.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X PATCH 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidRevoked.
application/json- response
id*stringuuidstatus*"revoked""revoked"curl -X DELETE 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "revoked"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
reason?stringlength <= 500Requested.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08/request' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidDenied.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08/deny'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidGranted.
application/json- response
id*stringuuidrole*stringYour side of the relationship.
"grantor""grantee"email*stringThe invited email (grantor view).
other_name*stringDisplay name of the other party.
status*stringinvited → accepted (the contact has a vault account) → sealed (the grantor sealed the vault keys to the contact) → requested → granted (approved, or the wait elapsed with no denial). A denial returns to sealed.
"invited""accepted""sealed""requested""granted""revoked"delay_hours*integerHours between an access request and automatic release, unless the grantor denies it.
244872vault_ids*array<>grantee_fingerprint*stringFingerprint of the contact public keys the grantor sealed to.
grantee_public_keys*Contact public keys (grantor view, after acceptance) to seal the vault keys to.
request_reason*stringrequested_at*stringdate-timerelease_at*stringWhen a pending request is released automatically.
date-timegranted_at*stringdate-timedenied_at*stringdate-timecreated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08/approve'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "role": "grantor", "email": "string", "other_name": "string", "status": "invited", "delay_hours": 24, "vault_ids": [ "ce59860b-d4b4-4beb-86e8-1b6dbe13411f" ], "grantee_fingerprint": "string", "grantee_public_keys": {}, "request_reason": "string", "requested_at": "2019-08-24T14:15:22Z", "release_at": "2019-08-24T14:15:22Z", "granted_at": "2019-08-24T14:15:22Z", "denied_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
vaults*array<>items*array<>curl -X GET 'https://ai.codai.ro/v1/vault/emergency/497f6eca-6276-4993-bfeb-53cbbbba6f08/vault-data'{ "vaults": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "encrypted_meta": "string", "sealed_key": {} } ], "items": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de", "revision": 0, "wrapped_key": "string", "ciphertext": "string", "kind": "string", "slug": "string", "agent_meta": null } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
prefix*stringFirst 5 hex characters of the SHA-1 of the password (k-anonymity).
^[0-9A-Fa-f]{5}$SUFFIX:COUNT lines (padded).
response?stringtext/plaincurl -X GET 'https://ai.codai.ro/v1/vault/hibp/range/string'"string"bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
items*array<>curl -X GET 'https://ai.codai.ro/v1/vault/rotation'{ "items": [ { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "slug": "string", "title": "string", "kind": "string", "interval_days": 0, "rotated_at": "2019-08-24T14:15:22Z", "due_at": "2019-08-24T14:15:22Z", "overdue": true, "breached": true, "breach_count": 0, "breach_checked_at": "2019-08-24T14:15:22Z", "snoozed_until": "2019-08-24T14:15:22Z", "provider": "stripe", "create_url": "string" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
interval_days?integer1 <= value <= 730provider?string"stripe""github_pat""openai""google_api_key""other"snoozed_until?stringdate-timeOK.
application/json- response
item_id*stringuuidinterval_days*integerprovider*string"stripe""github_pat""openai""google_api_key""other"snoozed_until*stringdate-timecurl -X PUT 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/rotation' \ -H 'Content-Type: application/json' \ -d '{}'{ "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "interval_days": 0, "provider": "stripe", "snoozed_until": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
item_id*stringuuidrotated_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/rotated'{ "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "rotated_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
via*stringWhere the item was used.
"windows_plugin""android_provider""ios_provider""desktop""phone"OK.
application/json- response
id*stringuuidlast_used_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/used' \ -H 'Content-Type: application/json' \ -d '{ "via": "windows_plugin"}'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
results*array<>items <= 500OK.
application/json- response
recorded*integernewly_breached*integercurl -X POST 'https://ai.codai.ro/v1/vault/rotation/breach-report' \ -H 'Content-Type: application/json' \ -d '{ "results": [ { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "count": 0 } ]}'{ "recorded": 0, "newly_breached": 0}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
OK.
application/json- response
derivers*array<>curl -X GET 'https://ai.codai.ro/v1/vault/derivers'{ "derivers": [ { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "provider": "aws_sts", "config": {}, "max_ttl_s": 300, "updated_at": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
provider*string"aws_sts""gcp_sa""cloudflare""github_app"config*max_ttl_s*integer300 <= value <= 43200OK.
application/json- response
deriver*curl -X PUT 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/deriver' \ -H 'Content-Type: application/json' \ -d '{ "provider": "aws_sts", "config": {}, "max_ttl_s": 300}'{ "deriver": { "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "provider": "aws_sts", "config": {}, "max_ttl_s": 300, "updated_at": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidDeleted.
application/json- response
deleted*booleancurl -X DELETE 'https://ai.codai.ro/v1/vault/items/497f6eca-6276-4993-bfeb-53cbbbba6f08/deriver'{ "deleted": true}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
handle*string1 <= length <= 80action*string1 <= length <= 4000purpose?stringlength <= 500provenance?string"user""agent""untrusted"chain?array<string>items <= 16task?stringttl_s?integer300 <= value <= 43200proof*Credential.
application/json- response
provider*string"aws_sts""gcp_sa""cloudflare""github_app"values*Short-lived credential fields. For the broker only: inject and redact, never pass to a model.
env*The same values keyed by environment variable name.
expires_at*stringdate-timecurl -X POST 'https://ai.codai.ro/v1/vault/derive' \ -H 'Content-Type: application/json' \ -d '{ "handle": "string", "action": "string", "proof": { "device_id": "string", "challenge": "string", "signature": "string" }}'{ "provider": "aws_sts", "values": { "property1": "string", "property2": "string" }, "env": { "property1": "string", "property2": "string" }, "expires_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
members*array<>curl -X GET 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/members'{ "members": [ { "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "email": "string", "name": "string", "role": "owner", "fingerprint": "string", "key_epoch": 1, "added_at": "2019-08-24T14:15:22Z" } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidOK.
application/json- response
candidates*array<>curl -X GET 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/candidates'{ "candidates": [ { "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "email": "string", "name": "string", "fingerprint": "string", "public_keys": { "kem": "string", "sign": "string" } } ]}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuiduserId*stringMember user id (uuid).
uuidapplication/json- body
role*stringowner is never assigned through this route.
"manager""editor""viewer"sealed_key*fingerprint*string8 <= length <= 200Added or re-sealed.
application/json- response
member*[key: string]?anycurl -X PUT 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/members/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Content-Type: application/json' \ -d '{ "role": "manager", "sealed_key": { "alg": "xwing-hkdf-xchacha20poly1305", "ct": "stringstringstri", "wrapped": "string" }, "fingerprint": "stringst"}'{ "member": { "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "email": "string", "name": "string", "role": "owner", "fingerprint": "string", "key_epoch": 1, "added_at": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuiduserId*stringMember user id (uuid).
uuidapplication/json- body
role*stringowner is never assigned through this route.
"manager""editor""viewer"Updated.
application/json- response
member*[key: string]?anycurl -X PATCH 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/members/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Content-Type: application/json' \ -d '{ "role": "manager"}'{ "member": { "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "email": "string", "name": "string", "role": "owner", "fingerprint": "string", "key_epoch": 1, "added_at": "2019-08-24T14:15:22Z" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuiduserId*stringMember user id (uuid).
uuidRemoved.
application/json- response
removed*booleanrotation_needed_at?stringdate-time[key: string]?anycurl -X DELETE 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/members/497f6eca-6276-4993-bfeb-53cbbbba6f08'{ "removed": true, "rotation_needed_at": "2019-08-24T14:15:22Z"}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
key_epoch*integer2 <= valueencrypted_meta*stringmembers*array<>1 <= itemsitems*array<>items <= 2000Rotated.
application/json- response
key_epoch*integer[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/rotate' \ -H 'Content-Type: application/json' \ -d '{ "key_epoch": 2, "encrypted_meta": "string", "members": [ { "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "sealed_key": { "alg": "xwing-hkdf-xchacha20poly1305", "ct": "stringstringstri", "wrapped": "string" } } ], "items": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "revision": 2, "wrapped_key": "string", "ciphertext": "string" } ]}'{ "key_epoch": 0}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
m*integer2 <= value <= 9scope?stringRequired when m is set.
"critical""high"Effective policy.
application/json- response
quorum*[key: string]?anycurl -X PUT 'https://ai.codai.ro/v1/vault/vaults/497f6eca-6276-4993-bfeb-53cbbbba6f08/quorum' \ -H 'Content-Type: application/json' \ -d '{ "m": 2}'{ "quorum": { "m": 2, "scope": "critical" }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
id*stringResource id (uuid).
uuidapplication/json- body
decision*string"approve""deny"proof*|Proof for a quorum vote, over approve/v4: codai-vault/approve/v4|<approval_id>|<action_hash>|<decision>|once|<approver_user_id>|<device_id>|<preview_digest>. device: one of YOUR enrolled device keys signs the message (software cli_ed25519 keys refused; t4 needs an attested device). passkey: a WebAuthn assertion whose challenge is base64url(SHA-256(the same message with device id passkey)). No match number: approvers are not at the agent.
Vote recorded.
application/json- response
status*string"pending""approved""denied"quorum*[key: string]?anycurl -X POST 'https://ai.codai.ro/v1/vault/approvals/497f6eca-6276-4993-bfeb-53cbbbba6f08/vote' \ -H 'Content-Type: application/json' \ -d '{ "decision": "approve", "proof": { "type": "device", "device_id": "string", "signature": "string" }}'{ "status": "pending", "quorum": { "m": 2, "approvals": 0, "needed": 0, "voted": true }}bearerAuthAuthorizationBearer <codai_…>A Kodwen API key (codai_ prefix). Ephemeral tokens from POST /v1/tokens are accepted only by the media surfaces matching their scope (/v1/realtime, /v1/audio/*, /v1/embeddings).
application/json- body
rules?array<>Draft rules; omitted = your current rules.
items <= 200default_tier?string"t1""t2""t3""t4"since?stringOldest use to replay (ISO).
date-timelimit?integer1 <= value <= 500Simulation.
application/json- response
evaluated*integerPast uses replayed.
changed*integerRows whose tier differs under the draft policy.
grants*"none"Grants and anomaly guards are not replayed.
"none"rows*array<>curl -X POST 'https://ai.codai.ro/v1/vault/policy/simulate' \ -H 'Content-Type: application/json' \ -d '{}'{ "evaluated": 0, "changed": 0, "grants": "none", "rows": [ { "event_id": "string", "at": "2019-08-24T14:15:22Z", "item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c", "title": "string", "agent": "string", "chain": [ "string" ], "mode": "string", "host": "string", "current_tier": "t0", "simulated_tier": "t0", "reasons": [ "string" ], "source": "event" } ]}OK.
application/json- response
keys*array<>curl -X GET 'https://ai.codai.ro/v1/vault/receipts/keys'{ "keys": [ { "kid": "string", "alg": "ES256", "pem": "string" } ]}