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.

ResourceReadWriteDelete
Contactscontacts:readcontacts:writecontacts:delete
Leadsleads:readleads:writeleads:delete
Projectsprojects:readprojects:writeprojects:delete
Bidsbids:readbids:writebids:delete
Pay applicationspay_apps:readpay_apps:writepay_apps:delete
Change orderschange_orders:readchange_orders:writechange_orders:delete
Site logssite_logs:readsite_logs:writesite_logs:delete
Time entriestime_entries:readtime_entries:writetime_entries:delete
Productsproducts:readproducts:writeproducts:delete
Documentsdocuments:readdocuments:writedocuments: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.

ScopeWhat it allows
bids:sendEmail a bid to a customer from the associated user account.
bids:accept_rejectTransition bids to accepted or rejected.
pay_apps:approveMove pay apps through the approval workflow.
change_orders:approveApprove or reject change orders.
ScopeWhat it allows
users:readList users in your organization (name, email, role).
webhooks:manageCreate, update, and delete webhook endpoints.
jobs:readCheck status and download results of async jobs the caller initiated.
jobs:writeEnqueue CSV imports/exports, PDFs, bulk ops; cancel or delete jobs.
offline_accessIssue 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).