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.
grant_type | required | Must be client_credentials. It is the only grant this endpoint issues. |
client_id | required | From your API access page. Can also be sent as HTTP Basic, which is what most OAuth libraries do. |
client_secret | required | The secret shown once when the credential was made. |
scope | optional | Space separated, to ask for LESS than the credential holds. Asking for more is ignored rather than refused. |
curl -s https://docubend.com/auth/oauth/token \
-d grant_type=client_credentials \
-d client_id=$DOCUBEND_CLIENT_ID \
-d client_secret=$DOCUBEND_SECRET
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();
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
{
"access_token": "dbt_EXAMPLEtokenNOTaREALoneQmZ7x2Kv9pR4sT6uW8yA",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "documents:read documents:write"
}
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.
| Scope | Methods | What it covers |
|---|---|---|
documents:read | GET, HEAD | List documents, fetch one, and download it as a PDF, an image, SVG, mermaid, .drawio or its own model. |
documents:write | POST, PUT, PATCH, DELETE | Create 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 once | What 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 hour | Long 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 call | Not 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 floor | A 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 it | Anything else means “revoked” has an hour-long asterisk on it. |
| Rotation leaves the old secret live for 24 hours | There 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 are | You reach | Anything else answers |
|---|---|---|
| A signed-in browser session | /api/…, the workspace routes | 401 at /api/v1 |
| A bearer token | /api/v1/… | 404 anywhere else |