# Credentials: Mosaic API reference

> Credentials in Mosaic’s local REST API: 11 routes, each with its method, path, fields, what it returns and the errors it names.

Source: https://mosaicbrowser.com/docs/api/credentials

**A profile’s data** REST API

## Credentials

Logins saved per profile, with their passwords and two-factor secrets encrypted through the OS keychain. No endpoint returns a secret; the only way one leaves the app is as keystrokes into the browser.

- [GETList a profile’s saved logins](#list-credentials)
- [POSTSave a login](#add-credential)
- [PATCHChange a saved login](#update-credential)
- [DELETEDelete a saved login](#delete-credential)
- [GETRead the browser’s own saved logins](#browser-logins)
- [POSTSave a login into the browser itself](#browser-login-add)
- [DELETERemove one saved login from the browser](#browser-login-remove)
- [GETRead the browser’s own bookmarks](#browser-bookmarks)
- [GETRead the domains this profile has visited](#browser-history)
- [GETGenerate a two-factor code](#credential-totp)
- [POSTType a login into the running profile](#fill-credential)

## [List a profile’s saved logins](#list-credentials)

GET`/api/v1/profiles/:id/credentials`

Returns summaries: label, site, username, and whether a password and a two-factor secret exist — never the values. `encryptionAvailable` reports whether this machine has a usable keychain; when it is false, secrets are stored as plain text and each record says so in its own `encrypted` field. Logins are held per profile rather than in one shared vault, because a shared vault makes it possible to fill the wrong account into the wrong session.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Returns

`{ credentials: CredentialSummary[], encryptionAvailable: boolean }`

## [Save a login](#add-credential)

POST`/api/v1/profiles/:id/credentials`

Encrypts the password and any two-factor secret before writing them. `totpSecret` is accepted as base32 in any spacing or case, or as a whole `otpauth://` URI copied from behind a QR code — all three are the same secret and all three work, so a user cannot silently save a broken credential and find out at the login screen.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `label` | `string` | What this login is for. Defaults to the site. |
| `site` | `string` | The site it belongs to. |
| `username` | `string` | Username or email address. |
| `password` | `string` | Plain text in, ciphertext at rest. |
| `totpSecret` | `string` | base32 or an `otpauth://` URI. A key that cannot decode, or that decodes to under 64 bits, is refused by name rather than stored — a truncated paste otherwise produces six wrong digits for ever. |
| `notes` | `string` | Anything else worth remembering. |

#### Returns

A `CredentialSummary`.

## [Change a saved login](#update-credential)

PATCH`/api/v1/profiles/:id/credentials/:credentialId`

Only the fields you send change. Omitting `password` leaves the stored one alone — that is the point of a patch here, because the editor shows a placeholder rather than the real value and an untouched form must not wipe the password with the empty string it appears to contain. Sending `totpSecret` as an empty string removes two-factor from the login.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |
| `credentialId` | `string` | The credential to change. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `label` | `string` | What this login is for. |
| `site` | `string` | The site it belongs to. |
| `username` | `string` | Username or email address. |
| `password` | `string` | Omit to keep the current one. |
| `totpSecret` | `string` | Empty string removes it. |
| `notes` | `string` | Anything else worth remembering. |

#### Returns

The updated `CredentialSummary`.

#### Errors

- 404 — no credential with that id on this profile.

## [Delete a saved login](#delete-credential)

DELETE`/api/v1/profiles/:id/credentials/:credentialId`

Removes it immediately. Credentials do not go to the trash — a password you asked to delete should be gone.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |
| `credentialId` | `string` | The credential to delete. |

#### Returns

`{ removed: true }`

#### Errors

- 404 — no credential with that id on this profile.

## [Read the browser’s own saved logins](#browser-logins)

GET`/api/v1/profiles/:id/browser/logins`

What Chromium itself saved while somebody used this profile, which is a different set from `/credentials` — that one is what Mosaic holds *for* the profile and fills on demand. Passwords are decrypted with the profile’s own key; a row that could not be decrypted comes back with `password: null`, which is not the same as an empty password and is never silently turned into one.

`passwordManagerOff` says the profile has Chromium’s password manager disabled, so an empty list there is a setting rather than a fact about the browser.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Returns

`{ logins: [{ realm, url, username, password, createdAt, timesUsed }], passwordManagerOff }`

#### Errors

- 404 — no profile with that id.

## [Save a login into the browser itself](#browser-login-add)

POST`/api/v1/profiles/:id/browser/logins`

Writes a row Chromium will offer to fill, as though the browser had saved it. The profile has to be stopped: Chromium keeps its password store in memory while it runs and rewrites the file on exit, so a login added underneath a live browser is discarded without a word when that browser closes — which is why this refuses rather than returning a 200 that did nothing.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `url`required | `string` | The sign-in page. Its origin becomes the realm. |
| `username`required | `string` | Or the email address the site signs in with. |
| `password`required | `string` | Plain text. Chromium encrypts it with this profile’s own key. |

#### Returns

`{ logins }` — the browser’s list, re-read after the write.

#### Errors

- 400 — the profile is running, or a field is missing.
- 404 — no profile with that id.

```
curl -X POST -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/login","username":"eugene","password":"…"}' \
  "$BASE/profiles/$ID/browser/logins"
```

## [Remove one saved login from the browser](#browser-login-remove)

DELETE`/api/v1/profiles/:id/browser/logins`

Scoped by `realm` and `username`, both required. An absent scope is a `400` and never "every login this browser has": on an irreversible route silence is refused rather than read as consent. Needs the profile stopped, for the same reason adding one does.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `realm`required | `string` | As `GET` reports it. |
| `username`required | `string` | The row’s username, exactly as reported. |

#### Returns

`{ removed, logins }`

#### Errors

- 400 — the profile is running, or a field is missing.
- 404 — no profile with that id, or no login matching that realm and username.

## [Read the browser’s own bookmarks](#browser-bookmarks)

GET`/api/v1/profiles/:id/browser/bookmarks`

Chromium’s bookmark tree, flattened, with the folder each one sits in. This is what the profile has accumulated by being used — distinct from the `bookmarks` field on the profile, which is what Mosaic *writes into* the bar before every launch. A profile that has never run has no bookmarks file and answers with an empty list rather than an error.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Returns

`{ bookmarks: [{ title, url, folder }] }`

#### Errors

- 404 — no profile with that id.

## [Read the domains this profile has visited](#browser-history)

GET`/api/v1/profiles/:id/browser/history`

Domains and visit counts, most visited first — not individual URLs. It is what the editor offers as suggestions when saving a login, and it is enough to answer "has this profile actually been used" without reading somebody’s browsing.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `limit` | `number` | How many domains. Default 40. A non-numeric value is a `400` naming the field. |

#### Returns

`{ domains: [{ domain, visits }] }`

#### Errors

- 400 — `limit` is not a whole number.
- 404 — no profile with that id.

## [Generate a two-factor code](#credential-totp)

GET`/api/v1/profiles/:id/credentials/:credentialId/totp`

Returns the current RFC 6238 code and how long it stays valid. Check `expiresIn` before using it: a code with a second left will be rejected between generating it and submitting the form, which is the usual reason a working authenticator looks broken.

With `?reveal=1` the reply also carries the setup key itself and an `otpauth://` URI for it. That is deliberate: a two-factor secret you cannot read back is one you cannot move to a phone or to another tool, and a profile is meant to carry everything an identity needs. The key is left out of the plain reply because the companion polls this route once a second while its panel is open, and a secret has no business in that traffic.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |
| `credentialId` | `string` | The credential to generate for. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `reveal` | `string` | `1` also returns `secret` and `otpauth`. |

#### Returns

`{ code, expiresIn, period }`, plus `{ secret, otpauth }` when `reveal=1`

#### Errors

- 404 — no credential with that id on this profile.
- 400 — this login has no two-factor secret, or it could not be decrypted on this machine.

## [Type a login into the running profile](#fill-credential)

POST`/api/v1/profiles/:id/credentials/:credentialId/fill`

Types one field into whatever is focused in the running browser, as real keystrokes with human timing — not `insertText` and not by setting `.value`, because a field that goes from empty to twenty characters in one event fires no `keydown` and a login form that receives no keystrokes is a cheap bot signal. Focus the field in the browser first: this deliberately does not guess which input is the password box, since guessing wrong types a password into a search field. Asking for `totp` when the current code is nearly expired waits for the next one rather than filling a dead code.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |
| `credentialId` | `string` | The credential to fill from. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `field` | `string` | `username`, `password` or `totp`. Default `password`. |

#### Returns

`{ field, length, expiresIn? }` — a character count, never the value.

#### Errors

- 400 — `field` is not one of the three.
- 400 — the profile is not running, or no tab has a focused field.
- 404 — no credential with that id on this profile.

```
curl -X POST -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"field":"totp"}' \
  "$BASE/profiles/$ID/credentials/$CID/fill"
```

## Run the API on your own profiles

The REST API and the MCP server are part of Pro: $49 a month, with the first 14 days free. Free has every browser feature and no API key.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
