Security Best Practices
Your API key is a bearer credential: whoever holds it is you, and can spend your tokens. This page is how to hold it well, and what we do with it on our side.
Handling your API key
Keys look like df_ followed by 48 hex characters — 24 random bytes generated on the
server. There is no user name, no password, and no OAuth exchange in front of it: the key alone
authenticates and authorizes the call, which is what makes it worth protecting.
Send it in a header, never in a URL
| Transport | Status |
|---|---|
X-API-Key: df_… | Preferred |
Authorization: Bearer df_… | Supported |
?apiKey=df_… in the query string | Removed — rejected as unauthenticated |
The query-string form was taken out deliberately. Full request URLs are recorded by Azure Functions logs, Application Insights and the front door, so a key passed that way is permanently written into diagnostics in plaintext. Request headers are not logged that way.
Keep it server-side
- Never ship a key to a browser or a mobile app. Anything in a client bundle is public, however it is obfuscated. Call DocButterfly from your backend, or from an automation platform that holds credentials for you.
- Never commit a key to source control, including private repositories — a repository's access list changes over the years, and history is forever.
- Use a secret store. Azure Key Vault, GitHub Actions secrets, an environment variable injected at deploy, or the connection field of your automation platform. Not a config file checked in beside the code.
- Always call over HTTPS. Every published endpoint is HTTPS; do not proxy the key over a plain-HTTP hop inside your own network either.
- One key per system, where you can. Separate keys make it possible to revoke the leaky one without stopping everything else.
How we store your key
We never store your key as plaintext. What we hold is an authenticator: a salted scrypt hash, with a per-key random salt, so a stolen copy of our storage does not yield working keys. A separate deterministic digest exists only to find your row; the scrypt hash is always verified afterwards, and where a server-side pepper is configured that lookup digest is an HMAC, so a storage breach on its own exposes nothing offline-attackable.
Beyond that, each account is in one of two key storage modes:
| Mode | What is kept | What that means for you |
|---|---|---|
retrievable |
The authenticator, plus an AES-256-GCM encrypted copy of the key. | You can show the key again later from the portal. Losing it does not force you to break every integration that already has it. |
hashOnly |
The authenticator and nothing else. | The key is visible exactly once, when it is issued. Lose it and the only recovery is a regenerate, which invalidates the old key. |
The encryption key for the retrievable copy lives in application settings, never in the same storage as
the ciphertext, and your account id is bound into the encryption as authenticated data — so a
ciphertext copied between records fails to decrypt rather than yielding somebody else's key. Every
retrieval is written to your own usage history as a key_retrieval entry, so a reveal you
did not perform is visible to you.
Which should you choose? hashOnly is the stricter posture and the right
default if your compliance position is that no recoverable copy of a credential may exist anywhere.
retrievable trades that for operational recovery. You can switch either way —
moving to hashOnly deletes the encrypted copy outright, it does not just hide it.
Ask us to change the mode on your account.
Rotating a key
Regenerate from Portal → API Key. One thing to plan for:
401 immediately.
So sequence it:
- List every place the key lives — flows, CI secrets, connectors, cron jobs, that one script on somebody's laptop.
- Regenerate, and capture the new key straight into your secret store.
- Update each consumer, then verify with the free probe:
GET /api/accountwith the new key returnsauthenticated: true.
Rotate immediately if a key was ever pasted somewhere you do not control, appeared in a log or a screenshot, or was held by someone who has left. A compromised key is not something to schedule — a rotation costs you an afternoon of updates, and a leaked key costs you your token balance and your document contents.
Watch your own account
The point of the ledger is that a compromise is visible before it is expensive. Every call writes a usage record with the operation, the tokens it cost, the caller IP, the user agent, the HTTP status, the duration and a correlation id — and, when a document was submitted, a SHA-256 of that document, so you can prove which file a call processed without us keeping the file.
- Portal → Usage — the same history, in the browser.
GET /api/account/ledger— charges, refunds, top-ups and allowance resets, newest first. Free and unmetered.GET /api/account/balance— what is left, split into monthly allowance and purchased tokens.
Unfamiliar caller IPs, calls at hours nobody works, or a balance falling faster than your volume explains: rotate first, investigate after.
Integration hygiene
- Treat webhook URLs as semi-secret. A published slug is useless without a key, but
there is no reason to post one publicly. Unpublishing a workflow returns
404to every caller immediately, and is reversible. - Do not log request bodies from integrations that carry documents. If you must, redact them — your logs are usually less protected than the document was.
- Fail closed on
403. A deactivated account is an account-level condition; retrying or swapping keys will not clear it, and a retry loop against a403is just noise. - Handle
401as “stop and alert”, not as a retry. An unattended flow that retries a dead key forever is how a broken integration stays invisible for a month.
The status codes and what each one means are in the Troubleshooting guide.
Browser extensions, and the installed app
Install the portal as an app and Chrome or Edge will tell you, under the puzzle-piece menu, that extensions can see and change information on this site. That is accurate. An installed web app is still a browser window, and it carries the extensions of the profile it was installed from.
No website can allow, block, or approve extensions — including this one. There is no browser API for it and no permit list a site can publish. Any vendor who tells you their web app blocks extensions is describing something else. So we do not say the vault is sealed; we say what is ours and what is yours.
What we control
- Transport and storage. Traffic is TLS; documents, the facts read from them and your saved answers live in your own account.
- What executes on the vault pages.
/Portal/Vaultand/Portal/VaultCaptureare served with a Content-Security-Policy whosescript-srcis'self'plus a per-request nonce — no third-party script origin, noeval, and no inline script that did not come from us in that response. It closes the ambient paths; it is not a defense against an extension, which runs with the browser's own privileges. - What the pages reveal. The capture page reports counts, never the facts it read.
What actually stops an extension
- Managed-device policy — the real control for a business. Chrome and
Edge are configured by the organization that owns the device, not by the sites it visits.
Set
ExtensionInstallBlocklistto*and list the approved extension IDs inExtensionInstallAllowlist, through Group Policy, Microsoft Intune, or the Google Admin console. This is the “block everything, then permit a list” model, applied at the only layer where it exists. - A separate browser profile for vault work. Free, immediate, and available to an individual: a profile with no extensions installed, used only for DocButterfly.
- A native app. A webview inside a native shell has no extensions at all. That is the sealed-container option, and it is a different product surface from the installed web app.
See what has access today
Open chrome://extensions (or edge://extensions) → Details
on any extension → Site access. An extension set to On all sites can read
every page you open, this one included; switching it to On click means it does nothing
until you ask it to. We cannot read that list from a web page, so we cannot show it to you here
— the browser deliberately keeps it out of a website's reach.
Think a key leaked?
Regenerate first — it takes seconds and invalidates the old key immediately. Then tell us, and we will help you read the usage history.
Regenerate my key Contact us