API reference
REST endpoints for third-party authorization v3 — client registration, consent, tokens, signed calls, the request lifecycle, revocation and operator-side disabling.
Conventions
| Item | Convention |
|---|---|
| Base URL | https://password.tminos.com |
| Format | JSON requests and responses (Content-Type: application/json), UTF-8 |
| Time | UTC, ISO 8601, second precision, e.g. 2026-10-23T08:30:00Z |
| Binary values | base64url, no padding |
| IDs | client_id starts with pmc_; grant_id and request_id are UUIDs |
| Errors | { "error": "<code>" }; see Errors |
| Body size limit | 400,000 bytes |
If you use the Swift SDK you never call these endpoints directly. This page is for developers implementing the protocol themselves or debugging an integration.
Authentication
| Method | Used for | Credentials |
|---|---|---|
| Account session | Registering clients, consent, device-side and user-side management | Authorization: Bearer <session token>, obtained through SRP sign-in, valid for 30 minutes |
| Client authentication | POST /api/oauth/token, POST /api/oauth/revoke | Public: PKCE verifier (token exchange) or approval-key signature (revoke). Confidential: client_secret |
| Signed call | Every third-party data endpoint | Authorization: Bearer <access_token> plus an approval-key signature; see Signatures |
Signatures
Apart from the token exchange, every third-party data call must carry both the access token and a signature from the grant's approval private key. A public client's revoke request is signed as well.
| Header | Value |
|---|---|
Authorization | Bearer <access_token> |
x-pm-timestamp | Current Unix time in seconds |
x-pm-nonce | A random string of 8–128 characters, never reused within the grant |
x-pm-signature | Ed25519 signature over the canonical string, base64url |
Canonical string (UTF-8, fields joined with \n, no trailing newline):
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{SHA256_HEX(BODY)}
METHOD: the HTTP method in uppercase.PATH: the path without the query string, e.g./api/oauth/requests/3f6b…/patch.BODY: the exact request body bytes you send. Do not modify the body after signing (including re-serializing it). With no body, use the empty string; its SHA-256 ise3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.- The timestamp must be within ±300 seconds of server time.
- A nonce can be used once per grant within a 10-minute window. A submitted nonce counts as used even if the signature check then fails.
GET /api/oauth/account HTTP/1.1
Host: password.tminos.com
Authorization: Bearer 3q2-7wEXAMPLEACCESSTOKEN
x-pm-timestamp: 1761177600
x-pm-nonce: n-0123456789abcdef
x-pm-signature: KngAgL1nChfn4NugEnlNlesKA8EtmrhpsL1GU9pU_Z0AcwkQRzP9iasdaFfe3nEVcvCGUT92eWHzzZCEX849DQ
You can reproduce this signature with the public test key in the test vectors.
Signature failures: signature_required (missing headers), signature_invalid (malformed values or bad signature), timestamp_out_of_window, nonce_replayed.
Register a client
POST /api/oauth/clients
Registers an integrating app. Authentication: account session. The Developer access page in MimiKey for Mac calls this endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–100 characters |
description | string | No | What the app does |
client_type | string | No | public or confidential; defaults to confidential |
platforms | string[] | No | Up to 10 entries, each 1–32 lowercase letters, digits, _ or -; display only |
redirect_uris | string[] | Yes | 1–10 URLs; see redirect URL rules |
brand | object | No | color (#RRGGBB), logo_url (https, at most 500 characters), homepage; other keys are ignored |
seal_public_key | string | No | Confidential only: a 32-byte X25519 public key. Public clients send their seal key with each authorization |
service_public_key | null | No | Retired; a non-empty value returns 400 mode_retired |
POST /api/oauth/clients HTTP/1.1
Authorization: Bearer <session token>
Content-Type: application/json
{
"name": "My AI Assistant",
"description": "Signs in to websites to reconcile statements",
"client_type": "public",
"platforms": ["ios", "macos"],
"redirect_uris": ["com.example.ai://mimikey/callback"],
"brand": { "color": "#1d4ed8", "logo_url": "https://cdn.example.com/logo.png", "homepage": "https://example.com" }
}
{
"client_id": "pmc_Xk3vQ9pL2mN8rT4w",
"client_type": "public",
"name": "My AI Assistant",
"platforms": ["ios", "macos"],
"brand": { "color": "#1D4ED8", "logo_url": "https://cdn.example.com/logo.png", "homepage": "https://example.com" }
}
For a confidential client the response also includes client_secret, returned this one time only; the server keeps only its hash.
Errors: invalid_input, invalid_client_type, invalid_redirect_uris, invalid_public_key, invalid_platforms, invalid_brand, mode_retired.
GET /api/oauth/clients
Lists the clients registered by the current account. Authentication: account session.
{
"clients": [
{
"client_id": "pmc_Xk3vQ9pL2mN8rT4w",
"name": "My AI Assistant",
"description": "Signs in to websites to reconcile statements",
"client_type": "public",
"redirect_uris": ["com.example.ai://mimikey/callback"],
"platforms": ["ios", "macos"],
"brand": { "color": "#1D4ED8" },
"has_seal_key": false,
"has_service_key": false,
"disabled": false,
"platform_disabled": false,
"active_grants": 3,
"created_at": "2026-10-23T08:00:00Z"
}
]
}
disabled means the developer disabled the client; platform_disabled means the platform operator did.
DELETE /api/oauth/clients/{id}
Disables a client. Authentication: account session. All of its grants are revoked, its tokens stop working and its unfinished requests expire; this cannot be undone. Returns { "ok": true }, or 404 client_not_found if the client does not exist or is already disabled.
Consent (MimiKey app side)
The MimiKey app calls these endpoints with the user's account session. Integrators do not implement them; they are listed to explain the protocol.
Deep links
The SDK starts MimiKey by opening one of these deep links:
| Purpose | URL |
|---|---|
| Authorize | mimikey://authorize?client_id=…&redirect_uri=…&state=…&code_challenge=…&code_challenge_method=S256&approval_key=…&seal_key=… |
| Handle a request | mimikey://request?id=…&client_id=…&redirect_uri=…&state=… |
| Parameter | Description |
|---|---|
client_id | Your registered ID |
redirect_uri | A registered redirect URL, matched exactly |
state | At least 16 random bytes; echoed back in the callback, and the SDK must discard any callback whose state does not match |
code_challenge | base64url(SHA-256(code_verifier)), 43–128 characters |
approval_key | This grant's approval public key (Ed25519, 32 bytes) |
seal_key | This grant's seal public key (X25519, 32 bytes) |
id | Request ID |
For callback parameters, see the callback summary.
GET /api/oauth/authorize/info
Data for the consent card. Query parameter: client_id.
{
"client": {
"id": "pmc_Xk3vQ9pL2mN8rT4w",
"name": "My AI Assistant",
"description": "Signs in to websites to reconcile statements",
"client_type": "public",
"brand": { "color": "#1D4ED8", "logo_url": "https://cdn.example.com/logo.png", "homepage": "https://example.com" },
"platforms": ["ios", "macos"]
},
"vaults": [
{ "vault_id": "8d4c…", "name": null, "kind": "personal", "default": true },
{ "vault_id": "b2a1…", "name": "Family", "kind": "shared", "default": false }
]
}
Only vaults the user owns are returned. For v2 compatibility, client also contains client_id, seal_public_key and service_public_key. Errors: 404 client_not_found, 403 client_disabled.
POST /api/oauth/authorize
Records the user's consent and issues an authorization code.
| Field | Required | Description |
|---|---|---|
client_id | Yes | |
redirect_uri | Yes | Must be one of the registered values |
state | No | Echoed back |
approval_key_public | Yes | 32-byte Ed25519 public key |
seal_public_key | Public: yes | 32-byte X25519 public key; for confidential clients, the registered seal key is used when omitted |
code_challenge | Public: yes | 43–128 base64url characters |
code_challenge_method | With a challenge | Only S256 |
policy | Yes | ask or trust |
selections | Yes | 1–32 entries of { "vault_id": "…", "mode": 1 }; mode: 2 returns 400 mode_retired |
Response: { "code": "…", "state": "…" }. The code is valid for 10 minutes and can be used once.
Tokens
POST /api/oauth/token
No session required. Public clients must not send client_secret (doing so returns 400 invalid_client).
Exchange an authorization code:
| Field | Required | Description |
|---|---|---|
grant_type | No | authorization_code (default) |
code | Yes | The code from the callback |
client_id | Yes | |
redirect_uri | Yes | Same value as at authorization |
code_verifier | Public: yes | 43–128 characters from [A-Za-z0-9-._~] |
client_secret | Confidential: yes |
POST /api/oauth/token HTTP/1.1
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "q8V0…",
"client_id": "pmc_Xk3vQ9pL2mN8rT4w",
"redirect_uri": "com.example.ai://mimikey/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}
{
"access_token": "3q2-7wEXAMPLEACCESSTOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "Zp9-EXAMPLEREFRESHTOKEN",
"refresh_expires_in": 2592000,
"grant_id": "6f1d2c3b-4a59-4e68-8b7c-9d0e1f2a3b4c",
"account_id": "0b7e…",
"vaults": [{ "vault_id": "8d4c…", "mode": 1 }],
"client_type": "public",
"account_identity_public_key": "9Xwg4YSHxLN9dr2JSRYL_wfRh_0iafGiNzQtT5blNys"
}
Important An authorization code is consumed by the first exchange attempt. After a failure such as a PKCE mismatch (
invalid_grant) or a redirect mismatch, the same code cannot be reused; the user must authorize again.
Refresh:
| Field | Required | Description |
|---|---|---|
grant_type | Yes | refresh_token |
refresh_token | Yes | |
client_id | Yes | |
client_secret | Confidential: yes |
The response matches the example above but omits vaults (a refresh never changes the grant's scope).
- Rotation: every refresh invalidates the refresh token it used and issues a new one, valid for 30 days from that refresh.
- Reuse detection: presenting an already-rotated refresh token returns
401 refresh_token_reusedand immediately revokes every token of that grant. When you get this, stop retrying automatically and ask the user to authorize again. - Refreshing does not require a signature, but the resulting access token is still useless without approval-key signatures.
| Response field | Description |
|---|---|
access_token | Access token, valid for 3600 seconds |
refresh_token | Refresh token, valid for 30 days (sliding) |
grant_id | Grant ID, part of the result and patch AADs |
account_id | The authorizing user's account ID |
vaults | Granted vaults (code exchange only) |
client_type | public or confidential |
account_identity_public_key | The account identity public key (X25519) for sealing update patches; null if the account has no identity key yet |
Revocation
POST /api/oauth/revoke
Revokes the grant a token belongs to. Idempotent: an unknown token also returns { "ok": true }.
| Client type | Body | Authentication |
|---|---|---|
| public | { "token": "<access_token>", "client_id": "…" } | Signed with that grant's approval private key (headers as in Signatures) |
| confidential | { "token": "…", "client_id": "…", "client_secret": "…" } | client_secret |
After revocation the grant's access and refresh tokens stop working immediately and every unfinished request becomes expired.
Account
GET /api/oauth/account
Signed call. Returns the authorizing user's account identity public key and the grant's vaults.
{
"account_id": "0b7e…",
"identity_public_key": "9Xwg4YSHxLN9dr2JSRYL_wfRh_0iafGiNzQtT5blNys",
"vaults": [
{ "vault_id": "8d4c…", "name": null, "kind": "personal", "mode": 1 }
]
}
identity_public_key can be null if the account has not generated an identity key yet; update_item is not possible in that case.
Requests
The request object
POST /api/oauth/requests, PUT …/patch and GET /api/oauth/requests/{id} return the same object (creation returns a subset of fields):
| Field | Description |
|---|---|
request_id | Request ID |
state | draft, pending, approved, completed, denied, expired, cancelled |
op | Operation (GET only) |
vault_id | Target vault; null for a cross-vault pick_item (GET only) |
item_id | Target item (GET only) |
created_at, expires_at, state_changed_at | Timestamps |
waiting_on, waiting_reason | Whose turn it is; see below |
existing | true when a create_item was deduplicated to an existing request (creation only) |
deny_reason | When denied, the reason given by the device, such as conflict (may be absent) |
result_enc | When completed, the sealed result (pick_item, get_item, list_items, create_item) |
picked_item | When a pick_item completes: { id, title, url, kind } |
created_item | When a create_item completes: { id, title, url, kind } |
saved_out_of_scope | true when a create_item was saved outside the grant; there is then no result_enc and no created_item |
updated_item | When an update_item completes: { id, revision }; there is no result_enc |
States and waiting
| State | waiting_on | waiting_reason | Meaning |
|---|---|---|---|
draft | client | patch | update_item is waiting for you to upload the patch |
pending (get_item / list_items, ask) | user | approval | Waiting for the user to approve |
pending (pick_item) | user | pick | Waiting for the user to pick |
pending (create_item) | user | input | Waiting for the user to fill in the safety card |
pending (update_item) | user | review | Waiting for the user to confirm the change |
approved | device | no_unlocked_device | Approved; waiting for any unlocked device to execute |
completed / denied / expired / cancelled | null | null | Finished |
Pick, create and update requests have no approved intermediate state: acting on the card is the approval. Expiry is evaluated lazily on read: an unfinished request past its expires_at is reported as expired.
POST /api/oauth/requests
Signed call. Creates a request.
| Common field | Description |
|---|---|
op | pick_item, get_item, create_item, update_item, list_items |
vault_id | Target vault; must be in the grant and in mode 1. May be null for pick_item |
reason | Optional; shown on the card, up to 500 characters (longer values are truncated), must not contain credentials |
A grant can have at most 100 unfinished requests (drafts included); beyond that the server returns 429 too_many_requests.
pick_item
| Field | Description |
|---|---|
vault_id | null or omitted: pick across every vault in the grant |
hint | Optional { "url", "title", "kind" }; only affects ordering |
{ "op": "pick_item", "vault_id": null, "hint": { "url": "https://bank.example.com", "kind": "login" }, "reason": "Sign in to Example Bank to reconcile statements" }
{ "request_id": "a1b2c3d4-…", "state": "pending", "waiting_on": "user", "waiting_reason": "pick", "expires_at": "2026-10-23T08:40:00Z", "state_changed_at": "2026-10-23T08:30:00Z" }
Valid for 10 minutes. The user must pick even under the trust policy.
get_item
| Field | Description |
|---|---|
vault_id | Required |
item_id | Required |
Valid for 10 minutes. Starts as pending under ask and as approved under trust.
create_item
| Field | Description |
|---|---|
vault_id | Required; the default save location |
kind | login (default), credit_card, bank_card, api_key, server, secure_note |
prefill | Optional { "title", "url", "username_hint", "extra_hint": { … } }; the v2 name site is also accepted |
{ "op": "create_item", "vault_id": "8d4c…", "kind": "login", "prefill": { "title": "Example Bank", "url": "https://bank.example.com/login", "username_hint": "me@example.com" }, "reason": "Sign in to Example Bank to reconcile statements" }
Valid for 30 minutes and always starts as pending. If the same grant and vault already have a pending create request for the same URL host, that request is returned with "existing": true.
update_item
| Field | Description |
|---|---|
vault_id | Required |
item_id | Required |
fields | Required. Names of the fields to change (1–32, each matching ^[a-z][a-z0-9_]*$, at most 64 characters), e.g. ["password"]. Visible to the server and previewed on the card |
Do not include patch_enc at creation. The request is created as a draft (valid for 60 seconds and never shown on the user's devices); then upload the patch with PUT …/patch.
{ "op": "update_item", "vault_id": "8d4c…", "item_id": "3f6b8f7e-…", "fields": ["password"], "reason": "Password was changed on the website; sync it to MimiKey" }
{ "request_id": "a1b2c3d4-…", "state": "draft", "waiting_on": "client", "waiting_reason": "patch", "expires_at": "2026-10-23T08:31:00Z", "state_changed_at": "2026-10-23T08:30:00Z" }
list_items
| Field | Description |
|---|---|
vault_id | Required |
The result contains metadata only (no passwords or notes). The Swift SDK does not expose this operation.
Upload an update patch
PUT /api/oauth/requests/{id}/patch
Signed call. Uploads the sealed patch for an update_item request in the draft state. The request becomes pending and its expiry resets to 30 minutes.
| Field | Description |
|---|---|
patch_enc | Sealed-box JSON string, at most 64 KiB |
Building the patch:
- Recipient:
account_identity_public_key(oridentity_public_keyfromGET /api/oauth/account) - AAD:
password-manager/oauth-patch/v1/{grant_id}/{request_id} - Plaintext:
{"patch":{"password":"new value"}}, containing only fields declared infields
{ "patch_enc": "{\"format\":1,\"epk\":\"PHld72bS…\",\"envelope\":{\"format\":2,\"nonce\":\"8Ovm4dzX…\",\"ciphertext\":\"9eSfVW9L…\"}}" }
Returns the request object with state pending and waiting_reason review. Errors: invalid_patch (malformed or too large), request_not_found, 409 request_not_draft (not a draft, expired, or not an update_item).
GET /api/oauth/requests/{id}
Signed call. Fetches a request, including its result once complete. Poll every 2 seconds, and check immediately when your app returns to the foreground.
{
"request_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"state": "completed",
"op": "pick_item",
"vault_id": null,
"item_id": null,
"created_at": "2026-10-23T08:30:00Z",
"expires_at": "2026-10-23T08:40:00Z",
"state_changed_at": "2026-10-23T08:31:12Z",
"waiting_on": null,
"waiting_reason": null,
"result_enc": "{\"format\":1,\"epk\":\"FiF4WQXs…\",\"envelope\":{…}}",
"picked_item": { "id": "3f6b8f7e-…", "title": "Example Bank", "url": "https://bank.example.com/login", "kind": "login" }
}
Open result_enc with the seal private key and the AAD password-manager/oauth-result/v1/{grant_id}/{request_id}:
{
"item": {
"id": "3f6b8f7e-9c1d-4e2a-8b3c-4d5e6f7a8b9c",
"revision": 4,
"record": {
"title": "Example Bank",
"username": "me@example.com",
"password": "correct horse battery staple",
"url": "https://bank.example.com/login",
"notes": "",
"kind": "login",
"extra": {},
"updated_at": "2026-10-23T08:00:00.000Z"
}
}
}
A list_items result is { "v": 2, "items": [{ "id", "meta": { "title", "username", "url", "kind", "updated_at" } }] }.
POST /api/oauth/requests/{id}/cancel
Signed call, no body. Cancels an unfinished request (draft, pending or approved). Returns { "request_id": "…", "state": "cancelled" }. A finished request returns 409 request_not_pending.
Legacy mode 2 endpoints
These endpoints exist only for read-only compatibility with existing mode 2 grants; new grants cannot use them:
| Endpoint | Description |
|---|---|
GET /api/oauth/vaults | Signed call; lists the grant's vaults and modes |
GET /api/oauth/vaults/{vault_id}/items | Signed call; returns the vault-key wrap and every item ciphertext in the vault. Mode 1 vaults return 403 mode_not_granted |
Device-side endpoints
The MimiKey app calls these with the user's account session; integrators never use them:
| Endpoint | Description |
|---|---|
GET /api/oauth/device/requests | Requests waiting for this account (drafts excluded) |
GET /api/oauth/device/requests/{id} | A single request (used when a deep link opens a card) |
POST /api/oauth/device/requests/{id}/decision | { "decision": "approve" or "deny", "reason": "conflict" }; card requests (pick, create, update) can only be denied here |
POST /api/oauth/device/requests/{id}/result | Uploads result_enc and the receipt: picked_item, created_item, updated_item or saved_out_of_scope |
User grant management
| Endpoint | Description |
|---|---|
GET /api/oauth/grants | The current user's grants |
GET /api/oauth/grants/{id}/history | A grant's usage history and 30-day counts |
POST /api/oauth/grants/{id}/policy | { "policy": "ask" or "trust" }; affects only new requests |
DELETE /api/oauth/grants/{id} | Revokes the grant, effective immediately |
Operator: disabling an integrator
Available only to the platform operator, authenticated with a separate administrator session that is isolated from user sessions.
| Endpoint | Description |
|---|---|
GET /api/admin/oauth/clients?query=&page=&page_size= | Paginated list of all clients (search by ID, name or owner email) with grant counts and 30-day request counts |
GET /api/admin/oauth/clients/{id} | Details, 30-day counts by operation and state, and the 50 most recent requests (no item contents or results) |
POST /api/admin/oauth/clients/{id}/disable | { "reason": "…" } (required, 1–500 characters) |
POST /api/admin/oauth/clients/{id}/enable | { "reason": "…" } |
Disabling a client:
- revokes all of its grants, invalidates all of its tokens, and expires all of its unfinished requests;
- makes later signed calls return
401 client_disabled; - makes
authorize/infoandauthorizereturn403 client_disabled, so the MimiKey apps refuse the client's deep links; - is recorded in the administrator audit log.
enable only clears the flag; it does not restore revoked grants. Users must authorize again.