Authentication and scopes
Every call your application makes for a user carries an access token that the user agreed to give you. This page explains how that agreement works, which scopes each route needs, how long tokens last, and what happens when the user takes the agreement back.
The consent chain
bind.co never hands over an inbox. It hands over records, and only the ones the inbox owner allowed:
- Use cases. When you register your application in the developer portal, you choose the use cases it serves, such as reading expenses.
- Scopes. Each use case needs a set of scopes. Your application may request only the scopes
registered for it, and scopes can be changed only while the application is a
DRAFT. - Consent. At
GET /oauth2/authorizethe user signs in with Google or Microsoft and approves the scopes you ask for, on bind.co's consent screen. The screen shows one line per data area with its access level: "view only" when you ask only for its:readscope, "view and edit" when you ask for its:writescope. - Records. The access token carries the approved scopes. Each route checks them, and answers only for the user the token belongs to.
The token names the user, so no route that acts for a user takes a user_id: one sent in the query
string, a form or a JSON body is refused with 400 invalid_request. The one exception is
GET /v1/webhook-events, which your application calls with its own credentials and filters by
user_id. To learn who signed in, call
GET /v1/oauth2/clients/me, which returns the user's public id and email address.
Scopes and the routes they open
Every operation in the API reference lists the scopes it checks under
x-required-scopes, and in its description. A route that needs several scopes needs all of them.
Each data area has two scopes. :read opens the routes that read: GETs and searches. :write
opens the routes that change records (create, edit, confirm, reject, restore, reset, merge, unmerge,
delete and the chaser actions) and includes :read: a token with contacts:write also reads
contacts. Ask only for what your application does.
| Scopes | What they open |
|---|---|
profile:read, profile:write |
Who the user is (GET /v1/oauth2/clients/me), the user's own profile and facts (/v1/profile/user, /v1/profile/user-profile), the companies they belong to (/v1/profile/company/..., /v1/profile/company-profile/...), and links to source emails (GET /v1/emails/{email_id}/web-link). |
contacts:read, contacts:write |
The people and businesses the user deals with (/v1/contacts/...), and the roles they play (/v1/roles/...). |
drafts:read, drafts:write |
Drafted emails and the goals behind them (/v1/drafts/...). |
drafts:send |
Sending a draft from the user's mailbox (POST /v1/drafts/{draft_id}/send), together with drafts:write. |
files:read, files:write |
The user's files and the documents read from them, including the documents.suggestion_available webhook event. The routes for files and documents are not in the public reference yet. |
operations:<type>:read, operations:<type>:write |
One operation type's records, for example operations:expenses:read for GET /v1/operations/expenses/{operation_id} and operations:expenses:write for PATCH on it. An operation type needs only its own scopes. |
Ask for profile:read in every authorization: it is how your application learns who the user is.
Sending is separate from drafting on purpose. A token with drafts:write alone can read and edit
drafts but cannot send one; POST /v1/drafts/{draft_id}/send answers 403 invalid_scope without
drafts:send. The outreach-approval routes of an operation type
(/v1/operations/{type}/chaser/outreach-approval/... and .../{operation_id}/chaser/outreach-approval/...)
show and decide on drafted messages, so they need drafts:read to list them and drafts:write to
decide, as well as the type's scope.
A token without a scope a route needs gets 403 invalid_scope, with hint.required_scope and
hint.granted_scopes. To add a scope, send the user through GET /oauth2/authorize again with the
wider scope: tokens already issued keep the scopes they were issued with.
A few routes need no token at all. GET /v1/operations lists the operation types, and
GET /v1/operations/{operation_type}/schema returns one operation
type's record shape. GET /.well-known/oauth-authorization-server advertises the OAuth endpoints,
grant types and every scope.
Tokens
The OAuth endpoints live at the API root, with no /v1: /oauth2/authorize, /oauth2/token,
/oauth2/revoke and /oauth2/introspect. Authenticate to the last three with your application's
client_id and client_secret, over HTTP Basic or as form fields. Only an APPROVED or IMMUTABLE
application obtains tokens; any other answers invalid_client. The form also accepts
client_public_id, a legacy alias of client_id; send client_id. /oauth2/authorize and
GET /v1/oauth2/authorize/preflight accept the same alias as a query parameter.
| What | Lifetime |
|---|---|
| Authorization code | 10 minutes, usable once |
| Access token | 1 hour (expires_in says exactly) |
| Refresh token | 30 days from when it was issued |
Authorization uses the code grant with PKCE, method S256 only; the
quickstart walks through it. Every token response carries
Cache-Control: no-store.
Refresh and rotation
Before the access token expires, exchange the refresh token for a new pair:
curl -u "$CLIENT_ID:$CLIENT_SECRET" https://api.bind.co/oauth2/token \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN"
Every refresh returns a new refresh token and retires the one you sent. Store the new one before you use the new access token.
If the answer to a refresh is lost, send the same refresh token again within 60 seconds: you get the
same new pair back, not a third one. After those 60 seconds, a retired refresh token is treated as
stolen. bind.co refuses it with invalid_grant and revokes every token your application holds for
that user, so a leaked token stops working for everyone who holds it. The user then has to go
through GET /oauth2/authorize again.
Revocation
POST /oauth2/revoke with token=... revokes a token you issued. Revoking an access token also
revokes its refresh token, and the other way round. It always answers 200, even for a token it
does not know, so it never reveals whether a token was valid.
When the user disconnects your application at bind.co, the consent ends at once: every token your
application holds for that user is revoked, and the next call with any of them is refused. An API
call answers 401 invalid_token, or 403 invalid_grant with hint.bind_subcode consent_revoked if
the consent ended while a token stayed live; a refresh answers invalid_grant. Your webhook endpoints receive consent.revoked, and no
further events are sent for that user. To work for them again, they must connect again.
Introspection
POST /oauth2/introspect with token=... tells you whether a token your application issued is
still valid:
{
"active": true,
"scope": "profile:read operations:expenses:write",
"client_id": "YOUR_CLIENT_ID",
"token_type": "Bearer",
"exp": 1792569600,
"iat": 1792566000,
"sub": "usr_0b6f6f5e-2a8e-4b7e-9d0c-3c1f2a4b5d6e"
}
sub is the user's public id. An unknown, revoked or expired token, or one issued to another
application, answers {"active": false} and nothing else. Introspection shares the token routes'
rate limit; if you check tokens often, cache the answer until exp.
Errors
The authentication errors you will meet most, all in the standard envelope; see Errors:
| Answer | When |
|---|---|
401 invalid_token |
No Authorization: Bearer header: WWW-Authenticate: Bearer realm="bind.co" names the scheme. |
401 invalid_token |
An Authorization header with another scheme, such as Basic: Authorization must use the Bearer scheme. |
401 invalid_token |
The access token is unknown, expired or revoked; WWW-Authenticate carries error="invalid_token". Refresh it, or send the user through authorization again. |
403 invalid_scope |
The token lacks a scope the route needs. |
403 invalid_grant, hint.bind_subcode consent_revoked |
The user withdrew consent for your application. |
403 access_denied, hint.bind_subcode client_not_approved |
Your application is not APPROVED or IMMUTABLE. |
invalid_client at /oauth2/token |
Wrong credentials, or the application cannot obtain tokens at its status. |