Skip to content
campaiDeveloper
campai.com

Authentication, authorization, and record selection are separate checks. A valid API key can lack a required permission. An authorized list request can return no matches. Changing a filter will not repair an authorization error.

Start with the same key, base URL, organization, and mandate your integration uses. Keep the response's HTTP status and structured code and message. Do not include your API key or personal record data in logs or support tickets.

What you’ll have at the end

A diagnosis that separates invalid credentials, missing scope, and a valid empty selection.

  1. Authentication
  2. Scope
  3. Operation permission
  4. Filters
Before you start
  • The failing operation and HTTP status
  • Access to inspect the key’s permissions and mandate restrictions
On this page

Call GET /apiKey/info, using the first-request example. If it fails, fix authentication before testing business operations.

The response includes the organization and the key's permission map. A missing scope or action does not grant that action. A successful key-info request does not prove that the key can read contacts, list mandates, or create invoices.

The current authentication implementation distinguishes an absent credential, which returns 401, from an invalid X-API-Key, which returns 400. Do not categorize every failure as 403 or keep rotating keys when the real problem is a request path.

  1. Compare ORGANIZATION_ID with the organization returned by key info. A key cannot access an unrelated organization merely because you know its ID.
  2. Confirm you are using the full organization ID in routes that require it. A five-character short ID works only where the operation explicitly supports one.
  3. With Mandates → Read, use List mandates to find an accessible mandate.
  4. Open the key's Restrictions in More → Developers → API Keys and compare its selection with MANDATE_ID.
Check the selected mandate as well as the permission scopes. Use your own organization’s mandate name.

Check the selected mandate as well as the permission scopes. Use your own organization’s mandate name.

API key Restrictions tab with Hauptmandant selected

The permissions object from key info does not describe every effective restriction. The mandate selection also matters. Have an organization owner adjust the intended restriction; do not remove restrictions merely to suppress an error.

Permissions distinguish Read, Insert, Edit, and Remove for a scope. Being able to list a resource does not imply that you can create or update it. The server checks permissions, not the HTTP verb alone: several list operations use POST while requiring Read.

For contact lists, make the contact type explicit. A key with Members → Read should request types: ['member']. It does not automatically have access to customers, suppliers, or employees. For finance and bookings, use the scopes relevant to those records.

This request reads at most one member. Replace YOUR_API_KEY, YOUR_ORGANIZATION_ID, and YOUR_MANDATE_ID directly in your chosen example. Each tab is complete and uses only cURL or the runtime’s built-in functions. First compare the organization with the key-info response as described above.

Example

curl --fail-with-body --silent --show-error --max-time 30 --globoff \
  --request POST 'https://cloud.campai.com/api/YOUR_ORGANIZATION_ID/YOUR_MANDATE_ID/crm/contacts/list' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "types": [
    "member"
  ],
  "limit": 1,
  "offset": 0,
  "returnCount": true
}'

The operation is POST /{organizationId}/{mandateId}/crm/contacts/list. Inspect contacts and count privately. A 200 response with an empty array is a valid result, not an authentication failure.

Remove optional selection criteria one at a time. Start with a positive limit, offset: 0, and the intended contact type. Remove searchTerm, selection, and userFilter while retaining the correct organization and mandate.

Observation Next check
No rows with limit: 0 Set a positive limit; a count-only request need not include records.
Empty page at a large offset Return to offset zero and request returnCount: true.
Unfiltered request works; filtered request does not Check the filter's field paths, comparators, value types, and JSON serialization.
Members appear in the app but not this request Check the selected mandate, contact type, key restrictions, and whether the app is using a broader user account.
Count differs from the app's total Compare all filters, views, contact types, and mandate selection before concluding that records are missing.

userFilter is a JSON-encoded string inside the JSON request body. Sending an object instead does not satisfy its contract. Follow Handling user filters and Find and filter receipts for complete examples.

Record the environment, method, operation path with sensitive IDs redacted where appropriate, HTTP status, error code, and time. Keep request bodies in restricted diagnostics only when necessary. Never log X-API-Key.

When a permission change fixes the request, reduce the key to the scopes and actions the integration actually uses, and run the same request again. Avoid solving a narrow read failure by granting Full access across the organization.

Check your result

Info fails
Resolve the key before changing filters.
Info works, scoped request fails
Check mandate and operation access.
Baseline returns records
Investigate the additional filter, view, or selection.
Continue with Make your first API request