1Password
A 1Password service account lets a browser session fill secrets (usernames, passwords, API keys) straight from your vault without the value ever reaching your automation code. It can also fetch and fill an emailed one-time code for sites that gate login on email 2FA, and save a new login to the vault when your automation creates an account. Browserless resolves the reference server-side, types it into the target field, and then locks the session down so the filled secret can't be read back or captured.
1Password is the first supported provider. You connect a 1Password service account once, then reference items by their op://Vault/Item/field path from inside a session.
This is useful when:
- You need to log in with real credentials but don't want them in your code, environment variables, or logs.
- You rotate secrets in 1Password and want every session to use the current value automatically.
- You need an auditable record of every credential use, without the secret itself ever being recorded.
- Your automation registers a new account and you want its login stored in 1Password, so later sessions fill it without your code handling the password again.
Authenticated Profiles replay a previously captured logged-in state. 1Password fills live credentials to log in fresh each time. Use a profile to skip the login; use 1Password when you must actually authenticate with current secrets. The two can be combined.
- A Browserless API token (available in your account dashboard)
- A 1Password service account with read access to the vaults you fill from, plus write access to a vault if you'll save new logins
- A CDP client like Puppeteer or Playwright installed
Visual learner? Check out this video Let Claude Log Into Sites Securely Using 1Password for a step-by-step tutorial.
How it works
A service account stores its 1Password token (encrypted at rest) and an optional domain allow-list, scoped to your API token. Inside a session you reference a secret by its op:// path and Browserless does the rest:
- Resolve — Browserless reads the referenced item from 1Password server-side using the stored service-account token.
- Fill — the resolved value is typed into the element you point at. The secret is never returned to your code, and never appears in CDP traffic, logs, or error messages.
- Lock down — once any secret has been filled, the session refuses to expose the page (see Security model), so the typed value can't be screenshotted or read back.
- Audit — the resolve is recorded against the service account: which
op://reference, on what origin, success or failure — but never the value.
Your automation only ever sends a reference (op://Vault/Item/field) and a target selector. The plaintext credential is resolved and typed inside Browserless; it never crosses back to the client.
Saving runs the other way. Browserless.saveSecret takes a username and password from your automation, stores them as a new Login item, and returns a reference that later fills use.
Adding a service account
In the dashboard and this guide these are 1Password service accounts (Profiles → 1Password Service Accounts). The REST API names the same object with integrationId and the /integrations/onepassword path.
- Dashboard
- API
The quickest way is the Profiles page in your Browserless dashboard, under 1Password Service Accounts:
Create a 1Password service-account token
In 1Password, create a service account and grant it read access to only the vault(s) holding the items you'll reference. Scope it as narrowly as possible — Browserless can resolve anything this service account can read. Copy the generated
ops_…token.To save new logins, also give it write access to the vault that should receive them. Decide this now: 1Password doesn't let you change a service account's vault access or permissions after it's created, so you can't upgrade a read-only service account later. You'd create a new one and add it alongside the old one.
Add a service account
Go to Profiles in the dashboard sidebar. Under 1Password Service Accounts, choose New Service Account. Pick the Provider (1Password), then paste the service-account token (
ops_…) and a Label to recognise it later. The token is encrypted at rest and is never shown again.Restrict allowed domains (optional)
By default the service account may fill on any site. To restrict it, turn on domain limiting and add the allowed origins (for example
https://app.example.com; an entry also matches its subdomains). Either way, secrets are only ever typed into publichttps://origins —http://and private/internal addresses are always refused.
You can also add a service account over the REST API. The service-account token is encrypted at rest and is never returned by this or any other endpoint.
curl -X POST "https://production-sfo.browserless.io/integrations/onepassword?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"label": "acme-prod",
"serviceAccountToken": "ops_REPLACE_WITH_YOUR_SERVICE_ACCOUNT_TOKEN",
"allowedDomains": ["https://app.example.com"]
}'
The response is the service-account record — note its id, you'll pass it when connecting:
{
"id": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
"label": "acme-prod",
"kind": "service_account",
"allowedDomains": ["https://app.example.com"],
"connectUrl": null,
"expiresAt": null,
"lastResolvedAt": null,
"createdAt": "2026-06-18T12:00:00.000Z",
"updatedAt": "2026-06-18T12:00:00.000Z"
}
id— the service account's identifier. Pass it, or thelabel, as?integrationId=when connectinglabel— unique within your Browserless account, so it works anywhere aniddoes, including?integrationId=and the management endpoints. Labels may contain spaces and characters such as&,+, or=, so URL-encode a label before putting it in a URLallowedDomains— optional; normalised to origins. When set, fills are limited to these origins and their subdomains. Omit it (or pass[]) to allow any publichttps://origin — the default- The response carries no token material — the service-account token can't be read back once stored
Filling a secret in a session
Connect with ?integrationId= set to the service account's id or label, navigate to the target page, then call the Browserless.loadSecret CDP method with the op:// reference and the field to fill.
Browserless.loadSecret is a CDP method, so use a CDP-capable client (Puppeteer, Playwright, or raw CDP).
- Puppeteer
- Playwright
- BrowserQL
import puppeteer from 'puppeteer-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const INTEGRATION_ID = 'op_int_0a1b2c3d4e5f6a7b8c9d0e1f';
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/chromium?token=${TOKEN}&integrationId=${INTEGRATION_ID}`,
});
const page = await browser.newPage();
await page.goto('https://app.example.com/login');
const cdp = await page.createCDPSession();
// Resolve op://Vault/Item/field from 1Password and type it into the field.
await cdp.send('Browserless.loadSecret', {
ref: 'op://Acme/login/username',
targetSelector: '#email',
});
await cdp.send('Browserless.loadSecret', {
ref: 'op://Acme/login/password',
targetSelector: '#password',
});
await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')]);
await browser.close();
import { chromium } from 'playwright-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const INTEGRATION_ID = 'op_int_0a1b2c3d4e5f6a7b8c9d0e1f';
const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io/chromium?token=${TOKEN}&integrationId=${INTEGRATION_ID}`,
);
const page = browser.contexts()[0].pages()[0] ?? (await browser.contexts()[0].newPage());
await page.goto('https://app.example.com/login');
const cdp = await page.context().newCDPSession(page);
await cdp.send('Browserless.loadSecret', {
ref: 'op://Acme/login/username',
targetSelector: '#email',
});
await cdp.send('Browserless.loadSecret', {
ref: 'op://Acme/login/password',
targetSelector: '#password',
});
await page.click('button[type="submit"]');
await browser.close();
loadSecret is also a BrowserQL mutation. Pass integrationId on the endpoint, then resolve and fill within the same query — alias one call per field. The BQL mutation uses selector (the CDP method uses targetSelector).
curl -X POST "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&integrationId=op_int_0a1b2c3d4e5f6a7b8c9d0e1f" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation Login { goto(url: \"https://app.example.com/login\") { status } username: loadSecret(ref: \"op://Acme/login/username\", selector: \"#email\") { ok error } password: loadSecret(ref: \"op://Acme/login/password\", selector: \"#password\") { ok error } }"
}'
Each loadSecret field returns { ok, error }; the resolved value is never in the response. ref may be an op://Vault/Item/field reference; selector is optional (the focused element is used if omitted).
loadSecret returns { ok: true } on success, or { ok: false, error: '<code>' } on failure (it never throws). See Failure modes for each error code and how to handle it.
The field now holds the secret value, but your code never saw it. Submit the form as usual.
Filling an emailed one-time code
Some sites send a one-time code (OTP) by email as a second factor. loadSecret can fetch that code from the inbox and type it in for you. It's read over IMAP inside Browserless, so like every other secret it's never returned to your automation, logged, or captured.
Instead of an op:// reference, pass a mail:// one that points at the inbox:
mail://<vault>/<item>/otp
<vault>/<item> name a 1Password Login item that stores the inbox's own IMAP credentials. That's the mailbox the codes are delivered to, not the site you're signing in to. Browserless reads that item with your connected service account, signs in to the mailbox over IMAP, waits for the code to arrive, extracts it, and fills your selector.
Point automation at a mailbox reserved for it, not a personal one. The resolver reads the newest message on each call, so a busy human inbox invites false matches. Any IMAP provider that supports implicit TLS on port 993 works, and Gmail is supported and tested.
Store the inbox as a 1Password Login item
In a vault your service account can read, add a Login item whose fields are the inbox's IMAP credentials:
| Field | Value |
|---|---|
username | The inbox address, e.g. otp-bot@example.com |
password | An IMAP app password for that inbox, not your normal login password |
host (optional) | IMAP host for a non-Gmail provider. Defaults to imap.gmail.com |
port (optional) | IMAP port. Defaults to 993, implicit TLS |
For Gmail, leave host/port off and create an app password with IMAP access.
Fill the code
Trigger the site's "email me a code" step, then call loadSecret with the mail:// reference and the code field. As with any secret you only ever see { ok, error }, never the code itself.
- Puppeteer
- BrowserQL
const cdp = await page.createCDPSession();
// Submit the login form first so the site emails a code, then fetch it.
await cdp.send('Browserless.loadSecret', {
ref: 'mail://Acme/login-inbox/otp?from=no-reply@example.com',
targetSelector: '#otp',
});
await page.click('button[type="submit"]');
mutation Verify {
# Trigger the code first by clicking the site's "email me a code" button.
otp: loadSecret(ref: "mail://Acme/login-inbox/otp", selector: "#otp") {
ok
error
}
}
Tune the lookup with query parameters on the reference:
| Option | Meaning |
|---|---|
?wait=<ms> | How long to wait for a fresh code. Default 45000, clamped to 5000–120000 |
?from=<substring> | Only accept a message whose sender contains this substring |
?pattern=<regex> | Custom extraction regex. Its first capture group, or the whole match, is the code. Without it a 4–8 digit numeric code is detected automatically |
Combine them like any query string, for example mail://Acme/login-inbox/otp?from=no-reply@example.com&wait=60000. Because from and pattern ride in the query string, percent-encode any special characters in their values. An unencoded & cuts the value short and a + is read as a space, so a \d+ regex must be written \d%2B.
loadSecret marks the inbox the moment it's called and accepts only a message that arrives after that. A stale code already sitting in the inbox is never reused, so trigger the "send code" step first, then call loadSecret promptly. It polls until the new code lands or wait runs out.
The poll runs inside the session, so connect with a ?timeout= larger than wait. Otherwise the session limiter can close the socket mid-poll. If no fresh code arrives in time, loadSecret returns error: "OtpTimeout". See Failure modes.
Filling an emailed code is a secret fill like any other, so the session lockdown applies the moment it succeeds. Autologin can perform this step for you: select an Email OTP inbox per site or apply one inbox to all selected sites. Candidates are Login items with username/password fields, including items without a website. The agent uses a separate emailOtp credential after triggering delivery; TOTP remains a distinct factor. Gmail is tested; other IMAP providers are best-effort. SMS and push approvals still require manual completion.
Saving a new login item
Browserless.saveSecret creates a Login item in a vault your service account can write to, then returns an op:// reference to the item's password. Use it when your automation creates an account, for example an agent signing up for a site. 1Password then holds the new login, and later sessions fill it without your code keeping the password around.
Your code supplies the password. Browserless doesn't generate one, because every site has its own password rules. Generate a value that meets them, save it, then fill the sign-up form from the vault with the returned reference, so the site receives exactly the value that's stored.
- Puppeteer
- BrowserQL
import { randomBytes } from 'node:crypto';
import puppeteer from 'puppeteer-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const INTEGRATION_ID = 'op_int_0a1b2c3d4e5f6a7b8c9d0e1f';
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/chromium?token=${TOKEN}&integrationId=${INTEGRATION_ID}`,
});
const page = await browser.newPage();
await page.goto('https://app.example.com/signup');
const cdp = await page.createCDPSession();
// Save before filling, because loadSecret needs the reference saveSecret returns.
const saved = await cdp.send('Browserless.saveSecret', {
title: 'app.example.com',
username: 'automation@example.com',
password: randomBytes(24).toString('base64url'),
website: 'https://app.example.com',
});
if (!saved.ok) throw new Error(`saveSecret failed: ${saved.error}`);
await page.type('#email', 'automation@example.com');
// Fill both password fields from the vault so the site gets the stored value.
await cdp.send('Browserless.loadSecret', { ref: saved.ref, targetSelector: '#password' });
await cdp.send('Browserless.loadSecret', { ref: saved.ref, targetSelector: '#confirm-password' });
await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')]);
await browser.close();
Response (saved):
{
"ok": true,
"ref": "op://4hb7cxzlwm5u3qtxvykw2rsg6e/jq2n5ty8vz3kc6mxa7rdh4wbfe/password"
}
With Playwright, send the same CDP calls through page.context().newCDPSession(page), as in Filling a secret in a session.
saveSecret is also a BrowserQL mutation with the same arguments. Pass integrationId on the endpoint, as you would for loadSecret.
curl --request POST \
--url "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&integrationId=op_int_0a1b2c3d4e5f6a7b8c9d0e1f" \
--header "Content-Type: application/json" \
--data '{
"query": "mutation Save { saved: saveSecret(title: \"app.example.com\", username: \"automation@example.com\", password: \"GENERATED_PASSWORD\", website: \"https://app.example.com\") { ok ref error message vaults } }"
}'
Response:
{
"data": {
"saved": {
"ok": true,
"ref": "op://4hb7cxzlwm5u3qtxvykw2rsg6e/jq2n5ty8vz3kc6mxa7rdh4wbfe/password",
"error": null,
"message": null,
"vaults": null
}
}
}
A BQL query can't pass one mutation's result into another, so it can't fill the returned ref in the same request. Run the sign-up in a second request instead. Each /chromium/bql request starts a new browser, so that request binds the same integrationId, opens the page with goto, then fills the password fields with the ref from the first response:
curl --request POST \
--url "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&integrationId=op_int_0a1b2c3d4e5f6a7b8c9d0e1f" \
--header "Content-Type: application/json" \
--data '{
"query": "mutation SignUp { goto(url: \"https://app.example.com/signup\") { status } type(selector: \"#email\", text: \"automation@example.com\") { time } password: loadSecret(ref: \"op://4hb7cxzlwm5u3qtxvykw2rsg6e/jq2n5ty8vz3kc6mxa7rdh4wbfe/password\", selector: \"#password\") { ok error } confirm: loadSecret(ref: \"op://4hb7cxzlwm5u3qtxvykw2rsg6e/jq2n5ty8vz3kc6mxa7rdh4wbfe/password\", selector: \"#confirm-password\") { ok error } click(selector: \"button[type=submit]\") { time } }"
}'
Response:
{
"data": {
"goto": { "status": 200 },
"type": { "time": 412 },
"password": { "ok": true, "error": null },
"confirm": { "ok": true, "error": null },
"click": { "time": 87 }
}
}
The reference uses vault and item IDs rather than titles. It keeps resolving after someone renames the item, and a reused or special-character title can't make it ambiguous. Store it with the account you created.
| Parameter | Type | Default | Description |
|---|---|---|---|
title | string | — | Title of the new Login item. 1Password allows duplicate titles, so pick one you'll recognize later, such as the site's host. |
username | string | — | Username or email stored in the item. |
password | string | — | Password stored in the item's concealed password field. It's never returned in the response. |
vault | string | The only vault the service account can see | Exact, case-sensitive title of the vault to save into. Required when the service account can see more than one vault. List the titles with includeVaults=true. |
website | string | None | Website stored with the item. Autologin only offers Login items that have a website, so set it if Autologin should find this login later. |
On failure saveSecret returns { ok: false, error, message } and never throws. When you omit vault and the service account can see several vaults, the response also lists their titles:
{
"ok": false,
"error": "VaultAmbiguous",
"message": "Several 1Password vaults are visible to this integration; pass \"vault\" with one of the titles listed in \"vaults\"",
"vaults": ["Acme", "Automation"]
}
See Failure modes for every save error code.
A few behaviors differ from loadSecret:
- No session lockdown. The password goes to the vault, not onto the page, so screenshots, Live URLs, and page reads keep working after a save. The lockdown starts with your first
loadSecretfill of any field, a username included. Browserless still scrubs the password from its logs and the session replay. - No origin check.
saveSecrettypes nothing into the page, so it works on any page, including before your first navigation. The service account's vault permissions decide where it can write, andallowedDomainsdoesn't apply. - Not idempotent. Each call creates a new item, even when one with the same title exists. Keep the returned
refinstead of saving the same account twice. - Audited. Each save adds an entry with outcome
savedand the new item's reference to the audit log.
The browser agent exposes the same method as its saveSecret command.
Security model
Once a secret has been filled in a session, Browserless treats the page as sensitive and disables every channel that could read the value back:
| Channel | Behaviour after a fill |
|---|---|
| Screenshots / PDFs / screencasts | Blocked (CaptureBlocked) |
| Live URLs (and their live-view input-event stream) | New connections refused; an already-streaming live view is torn down |
Page-content reads (BQL evaluate, html, text, querySelector, cookies) | Blocked (CaptureBlocked) |
Screen recording (Browserless.startRecording) | An in-progress recording is aborted and discarded on the first fill |
| Session replay (rrweb — DOM mutations) | Keeps recording, but the filled values are scrubbed from the replay before it's stored |
So the typed secret can't be screenshotted, captured on video, streamed to a live viewer, or read back — and any filled value is redacted from the rrweb session replay, logs, and CDP traces before they're persisted (raw and in common encodings).
Take any screenshot, PDF, or page-content read before the first Browserless.loadSecret call. After a fill, Browserless rejects them until the credential is off the page:
- One tab open: the next main-frame navigation, such as the redirect after sign-in, lifts the block.
- Several tabs open, or a single-page app that doesn't navigate after sign-in: call
Browserless.clearSecretsonce the filled fields are gone. The block covers the whole browser, and a navigation in one tab can't tell whether another tab still shows the secret.
BrowserQL has no clearSecrets mutation, so take BQL captures before the fill. Session replay stays scrubbed either way.
The cleanest way to get both a secure login and unrestricted screenshots/PDFs is to combine 1Password with Authenticated Profiles: in one session fill your credentials, sign in, then capture the signed-in state with Browserless.saveProfile. Every later session starts already logged-in via ?profile=<name>. Those sessions fill no secret, so captures and page reads work normally.
Don't want to script the login yourself? Autologin runs this entire one-time step for you — point it at a service account and it signs in to your allowed domains and saves the profile automatically.
// One-time: fill credentials from 1Password, sign in, save the auth state.
const cdp = await page.createCDPSession();
await cdp.send('Browserless.loadSecret', { ref: 'op://Acme/login/username', targetSelector: '#email' });
await cdp.send('Browserless.loadSecret', { ref: 'op://Acme/login/password', targetSelector: '#password' });
await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')]);
await cdp.send('Browserless.saveProfile', { name: 'acme-prod' });
// Everywhere after: reuse the profile — no fill, no lockdown, screenshots work.
// wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=acme-prod
This keeps secrets out of all but the initial login, and the service account stays the single source of truth when credentials rotate — just re-run the one-time step to refresh the profile.
Other guarantees:
- Origin safety — a secret is only ever typed into a public
https://origin;http://and private/internal addresses are always refused. A service account can optionally set an allow-list to restrict fills to specific origins (and their subdomains); with no list — the default — any public https origin is allowed. - No leakage of the value — the plaintext is never returned to the client and is scrubbed from logs and error messages.
- Encrypted at rest — the service-account token is encrypted and never returned by any API.
- Audit log — Browserless records every
loadSecretfill andsaveSecretsave with its outcome (success,saved, orfailure), never the secret value. A fill's entry has the reference and the page's origin. A successful save's entry has the new item's reference, plus the origin ofwebsitewhen you pass one.
Failure modes
When loadSecret returns { ok: false, error }, error is one of these codes. Resolve/service-account errors:
| Code | Meaning | What to do |
|---|---|---|
InvalidToken | 1Password rejected the service-account token. | Check the token; replace it if it is invalid or revoked, then update the service account. |
BadCredentialReference | The alias or reference is unknown, malformed, ambiguous, or uses unsupported field options. | The message names the cause: an alias the session does not know or a name with characters a reference cannot carry; more than one vault, item or field matching a name; or a field that cannot produce a time-based code. Use vault, item and field IDs instead of names when a name contains anything other than letters, digits, spaces, -, _ or ., or when names are not unique. |
VaultNotFound | No vault visible to the service account matches the reference. | Check the vault name or ID and the service account's access; retrying the same reference won't fix it. |
ItemNotFound | No item matches the reference in the vault. | Check the item name or ID and its location in the vault. |
FieldNotFound | The referenced field or section was not found in the item. | Check the field and section identifiers. |
VaultUnreachable | A network, timeout, or rate-limit failure prevented the 1Password request. | Retry with backoff; if it persists, check 1Password's status. |
UnknownResolveError | The lookup failed without a recognized cause. | Do not assume the failure is retryable; check the integration and reference, and contact support if it persists. |
CredentialNotResolved | Catch-all for a credential that could not be resolved, such as missing session credential context or an empty reference. | Check the session's service-account configuration and supply a reference. |
DomainNotAllowed | The page's origin isn't a permitted fill target — it isn't https://, it resolves to a private/internal address, or the service account has an allow-list the origin isn't on. | Use a public https origin; if the service account sets an allow-list, add the origin to it. |
CredentialIntegrationExpired | The Browserless integration has expired or is no longer available, for example after removal during a session. | Reconnect using an available, unexpired integration. A token rejected by 1Password is reported separately as InvalidToken. |
OtpTimeout | No fresh emailed code (a mail:// reference) arrived within the wait window. | Trigger the site's "send code" step right before calling loadSecret; raise ?wait= and the session ?timeout=. Check any from/pattern filter and that codes actually reach the inbox. |
Fill-target errors (the credential resolved, but it couldn't be typed in):
| Code | Meaning | What to do |
|---|---|---|
SelectorNotFound | The selector (targetSelector over CDP) matched no element. | Check the selector and that the field has rendered. |
TargetNotFillable | The target isn't a fillable <input> / <textarea>. | Point at a fillable element. |
NoFocusedElement | No selector was given and nothing is focused. | Pass a selector, or focus an input first. |
AuditWriteFailed | The fill was refused because its audit record couldn't be written (fail-closed). | Retry; if it persists, contact support. |
Save errors (saveSecret only):
| Code | Meaning | What to do |
|---|---|---|
IntegrationNotWritable | The service account can't create items in the chosen vault. | Create a 1Password service account with write access to the vault, add it to Browserless, and connect with its id or label. 1Password doesn't let you add permissions to an existing service account. |
VaultAmbiguous | You omitted vault and the service account can see more than one vault. | Retry with vault set to one of the titles in the response's vaults list. |
VaultNotFound | No visible vault has the exact title you passed, or the service account can't see any vault. | Check the title's spelling and case, or list the titles with includeVaults=true. |
ItemCreateFailed | A required parameter was missing or empty, or 1Password refused to create the item. | Pass non-empty title, username, and password strings. If they're all set, retry once, then contact support. |
saveSecret can also return InvalidToken, VaultUnreachable, CredentialNotResolved, CredentialIntegrationExpired, and UnknownResolveError, with the meanings listed above. CredentialNotResolved usually means you opened the session without ?integrationId=.
Managing service accounts
Your 1Password service accounts are managed through the same REST surface, scoped to your API token.
List service accounts
curl "https://production-sfo.browserless.io/integrations/onepassword?token=YOUR_API_TOKEN_HERE"
Returns an array of service-account records (no token material). Paginate with limit and offset.
Add includeVaults=true to also get the titles of the vaults each service account can see. Pass one as saveSecret's vault, and make sure the service account has write access to that vault, since listed vaults include read-only ones:
curl --request GET \
--url "https://production-sfo.browserless.io/integrations/onepassword?includeVaults=true&token=YOUR_API_TOKEN_HERE"
Response:
[
{
"id": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
"label": "acme-prod",
"kind": "service_account",
"allowedDomains": ["https://app.example.com"],
"connectUrl": null,
"expiresAt": null,
"lastResolvedAt": "2026-06-20T09:30:00.000Z",
"createdAt": "2026-06-18T12:00:00.000Z",
"updatedAt": "2026-06-18T12:00:00.000Z",
"vaults": ["Acme", "Automation"]
}
]
Browserless looks up the titles live from 1Password, so the option is off by default and covers only the first ten records in each response. Later records, and any lookup 1Password doesn't answer, return vaults: null. To get titles for more service accounts, page through with limit=10 and offset.
Get one service account
curl "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f?token=YOUR_API_TOKEN_HERE"
The path segment accepts the service account's id or its URL-encoded label, here and in the audit and delete endpoints below. includeVaults=true works on this endpoint too.
View the audit log
curl "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f/audit?token=YOUR_API_TOKEN_HERE"
Returns one entry per loadSecret fill or saveSecret save, newest first. Browserless never stores secret values, and the dashboard shows the same log for each service account.
Response:
[
{
"id": "op_audit_5e1c9a07b3d24f6e8a1b9c3d",
"integrationId": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
"profileId": null,
"secretRef": "op://4hb7cxzlwm5u3qtxvykw2rsg6e/jq2n5ty8vz3kc6mxa7rdh4wbfe/password",
"targetOrigin": "https://app.example.com",
"targetSelector": null,
"targetDescriptor": null,
"outcome": "saved",
"errorCode": null,
"createdAt": "2026-06-20T09:31:00.000Z"
},
{
"id": "op_audit_8b2d4f6a1c3e5a7b9d0f2e4c",
"integrationId": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
"profileId": null,
"secretRef": "op://Acme/login/password",
"targetOrigin": "https://app.example.com",
"targetSelector": "#password",
"targetDescriptor": "input[type=password]#password",
"outcome": "success",
"errorCode": null,
"createdAt": "2026-06-20T09:30:00.000Z"
}
]
A save types nothing into the page, so its entry has no targetSelector or targetDescriptor. Its targetOrigin comes from the website you passed, and it's null without one. A failed save has no item to point to, so its secretRef is Browserless.saveSecret and errorCode says why it failed.
Delete a service account
curl -X DELETE "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f?token=YOUR_API_TOKEN_HERE"
Deletion is permanent; the encrypted service-account token is removed.
FAQ & Troubleshooting
Can my automation read the secret value?
No. You send a reference (op://Vault/Item/field) and a target selector; Browserless resolves and types the value inside the session. It is never returned to the client, and after the first fill the page-read and capture channels are disabled so it can't be extracted.
Why is my screenshot/evaluate returning CaptureBlocked?
Run captures before the fill, or lift the block once the credential is off the page. With one tab open, the next main-frame navigation lifts it. Otherwise call Browserless.clearSecrets after the filled fields are gone.
A secret was filled in this session, and Browserless blocks captures and page-content reads so the value can't leak. See Security model.
Why did loadSecret return ok: false?
Common causes: the targetSelector didn't match an element on the page, the page isn't a permitted origin (not https://, a private address, or not on the service account's allow-list when one is set), or the op:// reference doesn't resolve (wrong vault/item/field, or the service account lacks access).
Why does saveSecret return IntegrationNotWritable?
Create a new 1Password service account with write access to that vault, add it to Browserless, and connect with its id or label.
Your current service account can read the vault but can't create items in it. 1Password sets a service account's permissions when it's created, so you can't grant write access to the existing one.
Does saveSecret block screenshots the way loadSecret does?
No. A save writes to the vault and puts nothing on the page, so captures and page reads keep working. The lockdown starts with your first loadSecret fill of any field, a username included, so take captures before that call.
Can it fill an emailed two-factor code?
Yes. Reference the inbox with a mail://Vault/Item/otp reference instead of an op:// one, where the item is a Login holding the inbox's IMAP credentials. Browserless waits for the newest code to arrive, extracts it, and fills your selector. The code is never returned to your automation. See Filling an emailed one-time code.
How is this different from Authenticated Profiles?
A profile replays a previously captured logged-in state (cookies/storage). 1Password logs in fresh by filling live credentials. Use a profile to skip login entirely; use 1Password when you must authenticate with current secrets — for example when the session needs to perform a real sign-in. They can be used together.
Why am I getting a 403 Forbidden error?
Your API token is missing or expired. Pass it as a ?token= query parameter in the WebSocket or HTTP URL. Verify the token in your account dashboard.
My script works locally but fails on Browserless
Local browser settings may differ from the Browserless environment. Use launch parameters to match your local setup (viewport, user agent, timezone). See launch parameters for the full list.
Further reading
- Autologin – let Browserless sign in with your 1Password credentials and save a reusable profile, no CDP scripting
- Authenticated Profiles – reuse a captured logged-in state instead of re-authenticating
- 1Password service accounts – create and scope the token this feature uses
- 1Password secret references – the
op://Vault/Item/fieldsyntax saveSecretmutation reference – every BrowserQL argument and response field for saving a login- Connection URL Patterns – full reference for connect-time query parameters