Skip to content

Permissions

Each key carries a list of permissions, chosen at creation. The format is module:action, always in English, and the actions are read, create, update and delete.

clients:read
clients:create
service_orders:update

GET /public/v1/me returns in key.permissions exactly what the key has.

Module Permissions
Clients
clients
clients:read clients:create clients:update clients:delete
Leads
leads
leads:read leads:create
Service orders
service_orders
service_orders:read service_orders:create service_orders:update service_orders:delete
Quotes
quotes
quotes:read quotes:create quotes:update quotes:delete
Sales
sales
sales:read sales:create sales:update
Finance
finance
finance:read finance:create finance:update finance:delete
Schedule
appointments
appointments:read appointments:create appointments:update appointments:delete
Products and stock
products
products:read products:create products:update products:delete
Services
services
services:read services:create services:update services:delete
Tasks
tasks
tasks:read tasks:create tasks:update tasks:delete
Team
team
team:read
Webhooks
webhooks
webhooks:read webhooks:create webhooks:update webhooks:delete
Events
events
events:read

Not every module accepts the four actions. sales has no delete because the panel does not delete a sale, and team and events are read only.

This table comes from the API contract, from the same file the panel and the server read. A new module shows up here on its own. Having the permission ticked on the key does not mean the route of that module already exists: the Reference is the list of what answers today.

GET /public/v1/me answers any valid key, with no specific permission required.

Keys created in the first version of the API are still valid: leads:write counts as leads:create, and leads:read and team:read stay the same.

What a key can actually do is where four things meet:

key permission ∩ member role ∩ member permissions ∩ company plan

One of them saying no is enough for the call to be refused. That is why a key with clients:create can still get a 403.

The key needs the permission the route requires. Without it, the answer is 403 with the code permission_missing. A public route with no declared permission is refused too: the default is to deny, never to allow.

Lead capture touches two modules, and the key needs the permissions of both:

  • POST /public/v1/leads requires leads:create. When the call opens the contact task, either the default one (a body without task) or the one you describe in task, it also requires tasks:create. Without it, the answer is 403 permission_missing and nothing is saved, not even the lead. With task: null no task is opened, and leads:create is enough.
  • If the phone number or the email was already on file, the response carries deduplicated: true. In that case, a key with neither leads:read nor clients:read gets only object and id, in both lead and task, and not the name, email, status and source of whoever was already in the base. The task comes back reduced because its default title is “Entrar em contato com” followed by the name of the existing record; in the dashboard, the title stays complete. A key that only captures does not read clients.

Service orders also write to other modules in two options, and the rule is the same:

  • POST /public/v1/service_orders with create_appointment: true and scheduled_start creates a calendar appointment, and also requires appointments:create. Without scheduled_start no appointment is created, as in the dashboard, and the permission is not required.
  • POST /public/v1/service_orders/{id}/complete with link_financial_transaction: true posts the income to finance, and also requires finance:create.

Without the second permission, the answer is 403 permission_missing and nothing is saved: neither the order nor the completion. Without those two options, the service order permission is enough.

The quote conversions follow the same rule, and each extra permission is only required when the effect really happens:

  • POST /public/v1/quotes/{id}/convert_to_sale always creates a sale, and also requires sales:create.
  • When the request ASKS for the finance posting (track_in_finance: true or an account in payment_plan.account_id), it also requires finance:create. When the field is left out and the sale goes to finance because the organization setting posts sales (the default), the permission is NOT required: that posting is an organization automation, like the contract on approval (see below), and the dashboard screen also lets someone who does not see finance convert that way.
  • Sending track_in_finance or the account requires the finance permission on the member: the dashboard conversion window hides both from whoever does not see finance (403 member_permission_denied).
  • decrement_stock: true decreases the stock of catalog products and does NOT require products:update: decreasing stock is part of selling, and the dashboard sale paths (including the seller portal) decrease it without the products permission. products:* is only required on the catalog routes themselves (products, variations, categories and stock).
  • POST /public/v1/service_orders/from_quote requires service_orders:create and quotes:read on the key, and the quotes permission on the member (the order is born with the quote content, and converting what the key cannot read would do more than the person would) and, with create_appointment: true and scheduled_start, also appointments:create, as creating a service order does.

Without the extra permission, the answer is 403 permission_missing and nothing is saved: neither the sale nor the service order.

Sales follow the same rule. The key needs the route sales permission and, when the effect really happens, the permission of the module it writes to:

  • Creating a sale that ASKS for the finance posting (track_in_finance: true or an account in payment_plan.account_id) requires finance:create. Without the field, the sale goes to finance through the company setting (the default), and that does NOT require finance:create: it is a company automation, the same one the screen applies to someone who does not see finance. The stock decrease (decrement_stock, true by default) does not require products:update: it is part of selling, as in the dashboard.
  • Receiving, reversing a payment, rescheduling installments, creating a new version and canceling the sale are SALES actions, like the dashboard Receipts tab, which a member without finance operates: they only ask for sales:update on the key and the sales permission on the member, even on a sale that is in finance or that decreased stock. What these actions change in finance (entries) and in stock (returns) is a sale automation.
  • The finance permission only comes in when the request CHOOSES something from finance: an account (account_id on a payment or payment_plan.account_id on the sale and on the version) requires finance:read and finance:create (on the version, finance:update) on the key, and the finance permission on the member. Deciding track_in_finance on creation requires the finance permission on the member. The dashboard screen hides both from whoever does not see finance.

Without the extra permission, the answer is 403 and nothing is saved.

Finance uses finance:read, finance:create, finance:update and finance:delete, and the member finance permission, in its three areas (entries, accounts and categories):

  • Reading is finance:read. Creating an entry, transfer, recurrence, installments, account and category is finance:create. Updating and mark_paid are finance:update. Deleting is finance:delete.
  • Roles are the dashboard ones: only owners and admins create, update and delete categories, and only they delete accounts. Any other role gets 403 member_permission_denied.
  • mark_paid on a sale installment records the SALE payment, and only asks for the finance permission: the payment is a consequence of the action, as in the dashboard, and does not ask for sales:*. It is the sales principle in the other direction: receiving through the sale does not ask for finance:*.
  • Every finance read schedules the sync of sales into finance, like the screen. It is a company automation and does not ask for sales:read.

Approving a quote can create a contract, when the organization turned on the automatic monthly contract. That creation is an organization automation, which runs the same way when the client approves through the portal, and it does not ask for an extra permission on the key: quotes:update is enough to approve.

The key acts as a member of the company, chosen at creation. Their role applies in the API exactly as it applies in the panel:

  • OWNER and ADMIN go through every area.
  • MEMBER depends on the permissions ticked in their profile, both to read and to write.
  • VIEWER never writes. They read the areas they have ticked.

A key never acts as a member whose role is above the role of whoever created it. An ADMIN does not issue a key that acts as an OWNER.

Within the MEMBER and VIEWER roles, the panel ticks area by area what the person can reach. The API honours the same ticks. If the person does not see finance on screen, the key that acts as them does not read finance through the API. The refusal is 403 with the code member_permission_denied.

The same ticks trim lists that bring different areas together. GET /public/v1/team returns the team the panel would show to the member: technicians only with the technicians permission, sellers only with the sales permission, and panel members and assistants with the tasks permission. OWNER and ADMIN see everyone. Someone without one of those permissions gets the list without those people, not an error.

This applies immediately. Change the ticks in the panel and the next call already feels it, with nothing to wait for. And if the member is suspended or removed from the company, the key stops authenticating: the answer is no longer a 403, it becomes the 401 from Authentication.

The plan needs to include the API, and the plan’s record quotas apply through the API just the same. The refusals are 403 with plan_feature_unavailable (the plan does not include the feature) and plan_limit_reached (the quota is used up). A subscription that is not in good standing answers 403 with subscription_inactive.

The error code says which layer refused:

  • permission_missing: the key refused. Create a key with the missing permission, or point the integration at a key that already has it.
  • member_permission_denied: the member’s role or permissions refused. Adjust that member’s access in the panel, or issue the key for another member.
  • plan_feature_unavailable: the plan refused. The company plan does not include that feature.
  • plan_limit_reached: the plan quota refused. The quota for the period is used up.
  • subscription_inactive: the subscription refused. Settle the subscription in the panel.
  • access_denied: a 403 with no mapped cause. The API refused and none of the codes above applies. If you get this one, send the request_id to support.

The three answers mean different things, and the difference matters while debugging.

401, authentication_error. The API does not know who is calling. Every authentication refusal returns the same answer, with the code api_key_invalid. See Authentication.

403, permission_error. The API knows who is calling and does not allow it. It is one of the four layers above.

404, invalid_request_error. The resource does not exist, or it exists and belongs to another company. An id from another company answers 404, never 403: answering 403 would confirm that the id exists somewhere. A path that is not an API route also answers 404, with the code route_not_found.

Read it straight: 401 is a key problem, 403 is a right problem, 404 is an address or id problem.

  • Tick only what the integration uses. An integration that reads clients does not need clients:delete.
  • One key per integration. Revoking one does not affect the others, and the usage log shows who called what.
  • Issue the key for the right member. If the integration only needs to read, issue it for a member who only reads: then not even a wrong tick on the key opens writing.