← Blog
Perfex CRMAPIIntegrations

Perfex CRM API Authentication: Tokens, Permissions, 401s

Perfex CRM API authentication fails for four separate reasons. Read the status code, then walk the token back through expiry, quota and per-resource grants.

A cron job that has posted leads into Perfex every ten minutes for eight months starts returning 401 at 04:00 on a Tuesday. Nothing was deployed. The key in the config file has not changed. The obvious conclusion - the API is broken - is almost always wrong, because Perfex CRM API authentication has four independent things that can refuse a request, and only one of them is the key itself. The other three are an expiry date somebody set and forgot, a quota that has already been spent, and a per-resource permission grant that never covered the verb you are now sending.

We build and sell the REST API module for Perfex CRM, so weigh the recommendations accordingly. The diagnostic order below is what we hand people in support tickets, and most of it transfers to any token-based API.

Perfex CRM API authentication starts with the status code

Perfex CRM does not ship a full REST API of its own, so whatever you are calling was added by a module. This one follows REST conventions and returns meaningful HTTP status codes, which makes the status the single most informative thing you have. If your HTTP client hides it behind a generic message string, fix that first - the rest of this article is unusable without it.

ResponseWhat it meansFirst thing to check
401The credential was not accepted at allHow the key is sent, and whether it has expired
403The credential was accepted, the action refusedThe per-resource grant for that exact verb
429Accepted and allowed, but going too fastThe X-RateLimit headers on the response
404 on a documented pathThe request never reached authenticationBase URL, rewrite rules, trailing segments
200 with an empty arrayAuthentication is fine, the scope is narrowStaff visibility, filters, field selection

That last row wastes the most time. An empty payload is not an authentication failure, and treating it as one sends people off to regenerate a key that was working perfectly.

The decision tree from a 401 back to the permission editor

Work through these in order. Each step is cheaper than the one after it, and stopping early is the point.

  1. Confirm the target install. A staging URL in a production config produces a textbook 401: the key is real and simply belongs to a different database.
  2. Check how the key is being sent. The module accepts three forms - a Bearer token in the Authorization header, an authtoken or api_key query parameter for URL-based access, or JWT. If you inherited a snippet that sets authtoken as a request header, that mismatch is your answer. Prefer the header in production regardless: query strings end up in access logs, proxy logs and pasted support tickets.
  3. Check the expiry date. Every key can carry one, and this is the cause behind the “it worked for eight months” story. An expired key is indistinguishable from a wrong key at the client.
  4. Check the request limit and the quota separately. A request limit throttles the rate; a quota is a budget that runs out. That is how an integration fails permanently with no code change - the period rolled over, or a second caller sharing the key spent it first.
  5. Check IP restrictions. Access can be limited by IP whitelist or blacklist with CIDR support, so a host migration or a container that came up on a new egress address trips it without anyone touching your code.
  6. Check whether you throttled yourself. Brute-force protection throttles repeated failed authentications per IP, so a retry loop on a bad key digs the hole deeper - and your debugging requests share the fate of production when both leave from the same address.
  7. Only now open the permission editor. If you reached this step, the credential is valid and the refusal is about what it is allowed to do.

Perfex CRM API token permissions are per resource and per verb

The permission model is a grid, not a switch. Each token gets Get, Create, Update and Delete granted or revoked per resource, so “the API key works” is not a meaningful statement. It works for the combinations somebody ticked, and for nothing else.

The classic failure: a token created with the Read-only button for a reporting dashboard, then reused six months later by a form handler that needs to create leads. Every GET keeps working, one POST fails, and because the working calls outnumber the failing one by a hundred to one, nobody suspects permissions. A failure isolated to a single endpoint and a single verb is a permission problem until proven otherwise.

The editor is built for this work. It covers every capability, Notes, Knowledge Base and Webhooks included, with Select all, Read-only and Clear all buttons, a live count of what is selected, and a per-feature toggle that grants a whole row at once. The sequence with the fewest surprises is Clear all, then Read-only, then add back only the write verbs the integration genuinely performs.

Custom tables are a second, separate grid

Database tables created by other Perfex add-ons are governed independently of everything above. They reach nothing by default: a built-in denylist plus an allowlist you populate yourself in API settings decide which tables a token may touch at all, and each granted table holds Read and Write separately - a token granted Read cannot write to it under any circumstances. So a GET against a custom table that succeeds while the matching POST is refused is not a broken endpoint. It is the Write half of that table’s grant sitting unticked.

One token per integration beats a shared master key

The tempting shortcut is a single key with everything enabled, pasted into every system that needs it. None of what that costs is visible on day one.

  • Revocation becomes all-or-nothing. Deleting a shared key stops your billing sync, your website forms and your reporting dashboard simultaneously. Per-integration keys mean you delete one and nothing else notices.
  • Usage reporting stops being usable. The module keeps per-API-key statistics and logs every request. With one shared key that report is a single line saying the CRM is busy; with one key per integration it names the caller that doubled its traffic last week or is quietly burning the quota.
  • AI agents inherit whatever you hand them. The built-in MCP server exposes 148 CRM tools over JSON-RPC 2.0, and the tool list is permission-filtered, so an agent can only see and do what its token allows. A master key given to an assistant is an assistant that can delete invoices. A token with Get everywhere and Create nowhere is one you can leave running while you evaluate it.
  • The custom-table allowlist works the same way. Per-token table grants only mean something if tokens are per-integration; otherwise the allowlist becomes the union of everything anything has ever needed.

Read the rate-limit headers instead of guessing

Every response carries X-RateLimit headers, so a client never has to guess where it stands. Back off on those numbers rather than a fixed one-second sleep, which is either wasting your evening or still too fast.

POST also supports an Idempotency-Key: an identical retry replays the stored response instead of re-executing, within a 24-hour window. That is what makes a network timeout survivable, because without it a request that succeeded server-side and died on the wire gets retried into a duplicate invoice.

If you are hitting limits because the sync makes one HTTP call per record, the fix is shape rather than allowance. Batch operations execute up to 50 in a single request, with per-operation results and continue-on-error.

What a valid token still will not show you

A perfectly authenticated request can return less than you expect, for reasons that have nothing to do with the key:

  • Staff visibility scoping. An optional staff-level data-visibility mode links a token to a staff member so the API scopes data exactly the way the admin panel does. Fewer rows is then correct behaviour, not a fault.
  • The response transformer. It can wrap responses in a standardised JSON format, filter response fields by query parameter and automatically strip sensitive fields for privacy, so a missing field may have been removed on the way out.
  • Field selection and date filters. ?fields=id,company and created_after / created_before do exactly what you asked, including when a client library sets them for you by default.

Before filing a bug about missing data, reproduce the call in the interactive Swagger playground inside Perfex, with the same token and no extra query parameters. If the data appears there, the difference lives in your client. The OpenAPI 3.0 document at GET /api/openapi settles the other common argument, describing all 74 paths and 144 operations with typed request fields.

Issue your next token like this

  1. Create a new key per integration, named after the system that will use it.
  2. In the permission editor hit Clear all, then Read-only, then tick only the Create, Update or Delete verbs that integration performs.
  3. For custom tables, grant the named tables only, with Write enabled solely where the integration writes.
  4. Set an expiry date matching the contract, the review cycle or the project end date.
  5. Set the request limit and quota above measured peak, then read the per-key statistics after a week and tighten them.
  6. Restrict by IP, using CIDR where the caller sits behind a range, whenever the source address is stable.
  7. Store the key in your environment or secret store and send it as Authorization: Bearer, never in a URL.
  8. Verify the token in the Swagger playground or Postman before you point production at it.

One rule to keep past all of this: never regenerate a failing key before you have read its status code and its per-key statistics. Regeneration destroys the only evidence of what went wrong, and hands you a brand new key carrying exactly the same wrong grants.