docubend
Documentation

Authentication

OAuth 2.0 client credentials, and nothing else. Any standard OAuth client library will do this without being told anything about us.

Getting a token#

OAuth 2.0 client credentials (RFC 6749 §4.4). Send your client id and secret, get a bearer token that lasts one hour. Any standard OAuth client library will do this without being told anything about us.

Credentials are made on your API access page. Ask for a token when you need one and keep it until it expires, a token per call is a thousand round trips you do not need.

Keep the token exchange on your server. A client secret in a browser bundle, a mobile app or a public repository is a credential somebody else has. Client credentials is the grant for your software talking to us. There is no flow here for a page talking to us on a visitor's behalf, because there is no third party and nothing to delegate.
Form fields
grant_typerequiredMust be client_credentials. It is the only grant this endpoint issues.
client_idrequiredFrom your API access page. Can also be sent as HTTP Basic, which is what most OAuth libraries do.
client_secretrequiredThe secret shown once when the credential was made.
scopeoptionalSpace separated, to ask for LESS than the credential holds. Asking for more is ignored rather than refused.
POST/auth/oauth/token
curl -s https://docubend.com/auth/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=$DOCUBEND_CLIENT_ID \
  -d client_secret=$DOCUBEND_SECRET
POST/auth/oauth/token
const BASE = "https://docubend.com";

async function token() {
  const r = await fetch(`${BASE}/auth/oauth/token`, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.DOCUBEND_CLIENT_ID,
      client_secret: process.env.DOCUBEND_SECRET,
    }),
  });
  if (!r.ok) throw new Error(`token: ${r.status}`);
  return (await r.json()).access_token;
}

const TOKEN = await token();
POST/auth/oauth/token
import os, requests

BASE = "https://docubend.com"

def token():
    r = requests.post(f"{BASE}/auth/oauth/token", data={
        "grant_type": "client_credentials",
        "client_id": os.environ["DOCUBEND_CLIENT_ID"],
        "client_secret": os.environ["DOCUBEND_SECRET"],
    }, timeout=30)
    r.raise_for_status()
    return r.json()["access_token"]

TOKEN = token()

Response

200
{
  "access_token": "dbt_EXAMPLEtokenNOTaREALoneQmZ7x2Kv9pR4sT6uW8yA",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "documents:read documents:write"
}
Why not three-legged OAuth. Because nobody is delegating anything. The thing on the other end is your own software acting for your own account. There is no third party, no consent to obtain and no other user's data in reach. An authorization-code flow would add redirect URIs, a consent screen and refresh tokens to answer a question nobody asked.

Scopes#

Two, and they are coarse on purpose. A scope per route reads like security and is really a maintenance burden nobody edits after the first day, the account's own capabilities behind it are the real limit.

ScopeMethodsWhat it covers
documents:readGET, HEADList documents, fetch one, and download it as a PDF, an image, SVG, mermaid, .drawio or its own model.
documents:writePOST, PUT, PATCH, DELETECreate documents, replace what is in them, place form fields, collect answers, and delete.

Scope is enforced by HTTP method, not by a list of routes. Anything that only reads is a read and everything else is a write, so a route added tomorrow is covered on the day it is added, and covered the safe way round.

Six rules#

The secret is shown onceWhat is stored is a SHA-256 of it. A leaked table must not be a pile of live keys, so “we cannot show it to you again” is true rather than a policy.
A token lasts one hourLong enough that a batch job does not spend its life at the token endpoint; short enough that a token scraped out of a log is usually already dead.
The plan is checked on every callNot only when the token was issued. A subscription that lapses stops the integration within the hour without anybody deleting a credential.
Scopes are a ceiling, never a floorA token can ask for less than its credential holds and never for more, and narrowing a credential bites the tokens already out there.
Revoking kills the tokens with itAnything else means “revoked” has an hour-long asterisk on it.
Rotation leaves the old secret live for 24 hoursThere is no order in which a new secret reaches every worker, container and cron entry without a gap. The window is what you deploy in. The it-got-out case has its own button and no window.

Browser or credential, never both#

A browser session cannot reach /api/v1 and a credential cannot reach anything else. Both are refused in one place, and both refusals say which door you are at.

Nothing in the docubend web app calls this API. The workspace has its own routes, so accepting a signed-in session here would buy nothing and leave a cookie-authenticated write surface for a cross-site form to aim at.

You areYou reachAnything else answers
A signed-in browser session/api/…, the workspace routes401 at /api/v1
A bearer token/api/v1/…404 anywhere else