Skip to content

Scopes

Every endpoint in the v1 API declares the scope a caller must hold to invoke it. Scopes are enforced uniformly for both API keys and OAuth access tokens by the requireApiScope middleware – the API never trusts a token’s tenant binding alone, and a request that lacks the required scope returns 403 scope_insufficient.

The required scope for each endpoint is shown in the API Reference.

Scopes fall into three groups: per-resource CRUD, workflow actions, and meta scopes.

10 resource families × 3 actions = 30 scopes. :read lets you list and fetch; :write lets you create and update; :delete lets you soft-delete. :write does not include :delete.

Resource Read Write Delete
Contacts contacts:read contacts:write contacts:delete
Leads leads:read leads:write leads:delete
Projects projects:read projects:write projects:delete
Bids bids:read bids:write bids:delete
Pay applications pay_apps:read pay_apps:write pay_apps:delete
Change orders change_orders:read change_orders:write change_orders:delete
Site logs site_logs:read site_logs:write site_logs:delete
Time entries time_entries:read time_entries:write time_entries:delete
Products products:read products:write products:delete
Documents documents:read documents:write documents:delete

State transitions and side-effecting actions get their own dedicated scopes – separate from :write – so you can grant edit access without granting the ability to send a customer email or move a pay app through approval.

Scope What it allows
bids:send Email a bid to a customer from the associated user account.
bids:accept_reject Transition bids to accepted or rejected.
pay_apps:approve Move pay apps through the approval workflow.
pay_apps:mark_paid Confirm payment of an approved pay application – manager only.
change_orders:approve Approve or reject change orders.
Scope What it allows
users:read List users in your organization (name, email, role).
webhooks:manage Create, update, and delete webhook endpoints.
jobs:read Check status and download results of async jobs the caller initiated.
jobs:write Enqueue CSV imports/exports, PDFs, bulk ops; cancel or delete jobs.
audit:read Read API audit log entries and consumption metrics for the calling tenant.
settings:read Read organization-wide configuration such as project templates.
settings:write Manage organization-wide configuration such as project templates. Admin-only.
offline_access Issue an OAuth refresh token so the app can stay connected without re-prompting.

When an OAuth client requests a scope, the user authorizing it can only grant scopes their own tenant role permits. A viewer consenting to your contacts:write request will only grant contacts:read – their role doesn’t allow writes anywhere.

Concretely:

  • The granted scope set is the intersection of what the client requested and what the user’s role can delegate.
  • Inspect scope on the token response to see what was actually granted.
  • Degrade gracefully when a scope you wanted is missing.

See OAuth 2.1 for the full flow.

Every v1 route is wrapped in requireApiScope("..."). When you call a route:

  1. The bearer token is validated and bound to a tenant.
  2. The required scope is checked against the token’s granted scopes.
  3. If the scope is missing, the request fails with 403 scope_insufficient – it never reaches the handler.

For API keys, the granted scopes are whatever the admin selected at key-creation time. For OAuth tokens, they’re the role-intersected subset granted at consent.

For OAuth clients, request the smallest set that covers your features. Users are far more likely to consent to contacts:read leads:read than to a sprawling list. You can always re-prompt for additional scopes later if a new feature needs them.

For API keys, the same principle applies – a reporting script doesn’t need :delete scopes, and a webhook receiver doesn’t need any scopes at all (delivery doesn’t authenticate to your token).