We are in official beta.

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
TransportStatus
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.
Watch the accidental disclosures. The common ones are not attacks: a key pasted into a support ticket, a screen share with the key visible in a terminal, a debug log that dumps request headers, and a screenshot of a working cURL command. Treat any of those as a leak and rotate.

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:

ModeWhat is keptWhat 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:

The old key stops working the moment the new one is issued. There is no overlap window and no grace period. Every flow, script and connector still holding the old key starts getting 401 immediately.

So sequence it:

  1. List every place the key lives — flows, CI secrets, connectors, cron jobs, that one script on somebody's laptop.
  2. Regenerate, and capture the new key straight into your secret store.
  3. Update each consumer, then verify with the free probe: GET /api/account with the new key returns authenticated: 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 404 to 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 a 403 is just noise.
  • Handle 401 as “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/Vault and /Portal/VaultCapture are served with a Content-Security-Policy whose script-src is 'self' plus a per-request nonce — no third-party script origin, no eval, 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

  1. 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 ExtensionInstallBlocklist to * and list the approved extension IDs in ExtensionInstallAllowlist, 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.
  2. A separate browser profile for vault work. Free, immediate, and available to an individual: a profile with no extensions installed, used only for DocButterfly.
  3. 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