Documentation / Developers

API reference

REST endpoints for third-party authorization v3 — client registration, consent, tokens, signed calls, the request lifecycle, revocation and operator-side disabling.

Conventions

ItemConvention
Base URLhttps://password.tminos.com
FormatJSON requests and responses (Content-Type: application/json), UTF-8
TimeUTC, ISO 8601, second precision, e.g. 2026-10-23T08:30:00Z
Binary valuesbase64url, no padding
IDsclient_id starts with pmc_; grant_id and request_id are UUIDs
Errors{ "error": "<code>" }; see Errors
Body size limit400,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

MethodUsed forCredentials
Account sessionRegistering clients, consent, device-side and user-side managementAuthorization: Bearer <session token>, obtained through SRP sign-in, valid for 30 minutes
Client authenticationPOST /api/oauth/token, POST /api/oauth/revokePublic: PKCE verifier (token exchange) or approval-key signature (revoke). Confidential: client_secret
Signed callEvery third-party data endpointAuthorization: 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.

HeaderValue
AuthorizationBearer <access_token>
x-pm-timestampCurrent Unix time in seconds
x-pm-nonceA random string of 8–128 characters, never reused within the grant
x-pm-signatureEd25519 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 is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
  • 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.

FieldTypeRequiredDescription
namestringYes1–100 characters
descriptionstringNoWhat the app does
client_typestringNopublic or confidential; defaults to confidential
platformsstring[]NoUp to 10 entries, each 1–32 lowercase letters, digits, _ or -; display only
redirect_urisstring[]Yes1–10 URLs; see redirect URL rules
brandobjectNocolor (#RRGGBB), logo_url (https, at most 500 characters), homepage; other keys are ignored
seal_public_keystringNoConfidential only: a 32-byte X25519 public key. Public clients send their seal key with each authorization
service_public_keynullNoRetired; 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.

The MimiKey app calls these endpoints with the user's account session. Integrators do not implement them; they are listed to explain the protocol.

The SDK starts MimiKey by opening one of these deep links:

PurposeURL
Authorizemimikey://authorize?client_id=…&redirect_uri=…&state=…&code_challenge=…&code_challenge_method=S256&approval_key=…&seal_key=…
Handle a requestmimikey://request?id=…&client_id=…&redirect_uri=…&state=…
ParameterDescription
client_idYour registered ID
redirect_uriA registered redirect URL, matched exactly
stateAt least 16 random bytes; echoed back in the callback, and the SDK must discard any callback whose state does not match
code_challengebase64url(SHA-256(code_verifier)), 43–128 characters
approval_keyThis grant's approval public key (Ed25519, 32 bytes)
seal_keyThis grant's seal public key (X25519, 32 bytes)
idRequest 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.

FieldRequiredDescription
client_idYes
redirect_uriYesMust be one of the registered values
stateNoEchoed back
approval_key_publicYes32-byte Ed25519 public key
seal_public_keyPublic: yes32-byte X25519 public key; for confidential clients, the registered seal key is used when omitted
code_challengePublic: yes43–128 base64url characters
code_challenge_methodWith a challengeOnly S256
policyYesask or trust
selectionsYes1–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:

FieldRequiredDescription
grant_typeNoauthorization_code (default)
codeYesThe code from the callback
client_idYes
redirect_uriYesSame value as at authorization
code_verifierPublic: yes43–128 characters from [A-Za-z0-9-._~]
client_secretConfidential: 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:

FieldRequiredDescription
grant_typeYesrefresh_token
refresh_tokenYes
client_idYes
client_secretConfidential: 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_reused and 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 fieldDescription
access_tokenAccess token, valid for 3600 seconds
refresh_tokenRefresh token, valid for 30 days (sliding)
grant_idGrant ID, part of the result and patch AADs
account_idThe authorizing user's account ID
vaultsGranted vaults (code exchange only)
client_typepublic or confidential
account_identity_public_keyThe 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 typeBodyAuthentication
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):

FieldDescription
request_idRequest ID
statedraft, pending, approved, completed, denied, expired, cancelled
opOperation (GET only)
vault_idTarget vault; null for a cross-vault pick_item (GET only)
item_idTarget item (GET only)
created_at, expires_at, state_changed_atTimestamps
waiting_on, waiting_reasonWhose turn it is; see below
existingtrue when a create_item was deduplicated to an existing request (creation only)
deny_reasonWhen denied, the reason given by the device, such as conflict (may be absent)
result_encWhen completed, the sealed result (pick_item, get_item, list_items, create_item)
picked_itemWhen a pick_item completes: { id, title, url, kind }
created_itemWhen a create_item completes: { id, title, url, kind }
saved_out_of_scopetrue when a create_item was saved outside the grant; there is then no result_enc and no created_item
updated_itemWhen an update_item completes: { id, revision }; there is no result_enc

States and waiting

Statewaiting_onwaiting_reasonMeaning
draftclientpatchupdate_item is waiting for you to upload the patch
pending (get_item / list_items, ask)userapprovalWaiting for the user to approve
pending (pick_item)userpickWaiting for the user to pick
pending (create_item)userinputWaiting for the user to fill in the safety card
pending (update_item)userreviewWaiting for the user to confirm the change
approveddeviceno_unlocked_deviceApproved; waiting for any unlocked device to execute
completed / denied / expired / cancellednullnullFinished

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 fieldDescription
oppick_item, get_item, create_item, update_item, list_items
vault_idTarget vault; must be in the grant and in mode 1. May be null for pick_item
reasonOptional; 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

FieldDescription
vault_idnull or omitted: pick across every vault in the grant
hintOptional { "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

FieldDescription
vault_idRequired
item_idRequired

Valid for 10 minutes. Starts as pending under ask and as approved under trust.

create_item

FieldDescription
vault_idRequired; the default save location
kindlogin (default), credit_card, bank_card, api_key, server, secure_note
prefillOptional { "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

FieldDescription
vault_idRequired
item_idRequired
fieldsRequired. 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

FieldDescription
vault_idRequired

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.

FieldDescription
patch_encSealed-box JSON string, at most 64 KiB

Building the patch:

  • Recipient: account_identity_public_key (or identity_public_key from GET /api/oauth/account)
  • AAD: password-manager/oauth-patch/v1/{grant_id}/{request_id}
  • Plaintext: {"patch":{"password":"new value"}}, containing only fields declared in fields
{ "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:

EndpointDescription
GET /api/oauth/vaultsSigned call; lists the grant's vaults and modes
GET /api/oauth/vaults/{vault_id}/itemsSigned 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:

EndpointDescription
GET /api/oauth/device/requestsRequests 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}/resultUploads result_enc and the receipt: picked_item, created_item, updated_item or saved_out_of_scope

User grant management

EndpointDescription
GET /api/oauth/grantsThe current user's grants
GET /api/oauth/grants/{id}/historyA 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.

EndpointDescription
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/info and authorize return 403 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.

Last updated: 2026-10-23