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