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 |