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.

bind.co never hands over an inbox. It hands over records, and only the ones the inbox owner allowed:

  1. Use cases. When you register your application in the developer portal, you choose the use cases it serves, such as reading expenses.
  2. 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.
  3. Consent. At GET /oauth2/authorize the 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 :read scope, "view and edit" when you ask for its :write scope.
  4. 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.