#Authentication and Permissions
Status: Available — keys, scopes, permissions, the IP list. Planned — test keys and the sandbox.
Purpose: how integrations and keys work, what scopes mean, how the effective permissions of a request are computed, how to rotate and revoke keys, and what to do when a key leaks.
#The model: tenant → integration → keys
Tenant
└── Integration "My CRM" (a service account, no dashboard login)
├── integration role (a set of permissions)
├── Key A (scopes, expiry, IP list, mode)
└── Key B (issued during rotation; Key A keeps working for up to 24 h)
- An integration is a tenant service account. You cannot sign in to the dashboard with it; it does not appear in operator pickers, mailings, or seat counts. It has its own role and access to all of the tenant's conversations.
- Keys belong to an integration. Rotating a key does not change the integration identifier, so your logs and audit trail stay continuous.
- Every integration is bound to an owner employee, the person who created it. The integration's permissions depend on that employee's current permissions (see below).
- A tenant can have at most 20 active keys.
- An employee with the
integrations.managepermission (by default, the owner and the manager) can create integrations and keys. This permission is separate from managing the keys of external providers that the platform itself uses.
#Key format
sap_<cell>_<mode>_<identifier>_<secret>
| Part | Values | Meaning |
|---|---|---|
sap | constant | Prefix. Secret scanners use it to find leaked keys. |
cell | ru, en, id | The cell in which the key is valid. |
mode | live, test | Production or test (the sandbox is Planned). |
identifier | 12 base32 characters | The public key identifier (key_id). Visible in the dashboard and logs. |
secret | about 43 base62 characters | The secret part: 32 random bytes. |
Example (deliberately fake):
sap_ru_test_EXAMPLE0KEYID_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEX
The platform stores only an irreversible hash of the secret with the cell's server-side pepper. A database leak without the pepper does not reveal the keys.
#How to send the key
Only in the Authorization header, and only over TLS:
GET /api/public/v1/me HTTP/1.1
Host: formula-cream.pro
Authorization: Bearer sap_ru_live_EXAMPLE0KEYID_…
- You must not send the key in the URL (
?key=…), the request body, or a cookie. - In v1, per-request signing is neither required nor supported: protection is built on a Bearer key over TLS. A hardened mode based on the client's public key (Ed25519) is not supported in v1. The
signature_invalidcode is reserved for it and is not returned in v1. - A browser or a mobile app must not receive the key: use the client interfaces (Introduction).
#Key lifecycle
| Event | How it works |
|---|---|
| Issuance | Dashboard: Settings → API. The key is shown once; save it immediately. The event is written to the audit log and a notification goes to the tenant owner. |
| Storage | The customer's responsibility: a secret store or environment variables. Not in a repository, not in a frontend, not in logs. |
| Expiry | 365 days by default; you cannot set more than 365. The expiry is visible in GET /me (key.expires_at). The api_key.expiring webhook event arrives 30 and 7 days before expiry. A key that has expired gets the same response as a nonexistent one: 401 invalid_api_key. |
| Rotation | Issue a new key for the integration, switch your systems over, then revoke the old one. During rotation the old key keeps working for up to 24 hours, so the switch does not have to be instantaneous. |
| Revocation | Dashboard → the key → "Revoke". Takes effect within 60 seconds at most (the verification result is cached for up to a minute; the cache is cleared on revocation). Revocation is irreversible. |
| Tracking | For each key, the dashboard shows the time and (in hashed form) the address of the last use. |
#Step-by-step rotation without downtime
- Issue a new key for the same integration with the same scopes.
- Deploy the new key to your secret store and restart the services.
- Use "API usage" in the dashboard to confirm that the old key is no longer called.
- Revoke the old key. If you forget, it expires on its own at the end of the overlap window.
#Scopes
A scope is a permission you give to a key. Grant the minimum necessary. The "Paid" column shows whether the tenant's wallet is charged. The "PII" column shows whether the responses contain personal data of the tenant's customers.
| Scope | What it grants | Paid | PII |
|---|---|---|---|
conversations:read | Conversations and messages | no | yes |
conversations:write | Receiving a customer message from your channel, an operator message, handoff, closing, taking over and releasing the AI | no | yes |
ai_seller:invoke | The AI seller's reply to a customer message from your channel | yes | yes |
leads:read | Leads and contacts | no | yes |
leads:write | Creating and editing a lead, accepting into an external CRM, notes, tasks, confirming agent action requests, writing an external score (PUT /leads/{id}/score), the status map (PUT /crm/status-map) | no | yes |
leads:score | Lead scoring by our model | yes | yes |
analytics:read | Traffic aggregates, sources, funnel, visibility | no | no |
analytics:write | Ingesting events and offline conversions; pseudonymous identifiers only | no | no (see note 2) |
dashboards:read | Ready-made "Overview" and "Pulse" views | no | no |
articles:read | Articles, revisions, publication statuses | no | no |
articles:write | Editing, approval, publishing | no | no |
articles:generate | Ordering article generation | yes | no |
strategist:read | Reports, the topic queue, chat actions | no | no (note 1) |
strategist:write | Approving and rejecting topics, confirming and canceling strategist actions | no | no |
strategist:chat | A turn in the conversation with the strategist | yes | no (note 1) |
webhooks:manage | Event subscriptions, the delivery log | no | no (note 6) |
Notes.
- The strategist works with aggregates and, by design, does not see personal data; this is verified when the API is released.
- Event ingestion accepts only pseudonymous identifiers:
anonymous_idanduser_idare your own pseudonyms. Email, phone, and name inpropertiesare rejected with422 validation_failed(a JSON Pointer identifies the field), so this scope is not classified as PII. POST /leads/{id}/scoreis paid and requiresleads:score. Writing the external CRM's own score (PUT /leads/{id}/score) is free and requiresleads:write.- Approving a strategist topic and confirming a strategist action additionally require the permission for what is being proposed and an available budget. Approving a topic does not start article generation.
GET /me,GET /wallet, andGET /operations/{id}are available to any valid key and require no scope (GET /meandGET /walletalso work while exchange is suspended; see Wallet); operations are visible only to the integration that created them.- A subscription with
include_pii: trueadditionally requires the integration to haveleads:readand/orconversations:read(Webhooks). - The personal data disclosure log is visible to the tenant in the dashboard and through
GET /disclosures(retained for 3 years). Which permission a key needs for this request is not fixed in v1 and will be specified in OpenAPI. For details, see Cells and data.
#Mapping scopes to endpoints
| Scope | Endpoints |
|---|---|
conversations:read | GET /conversations, GET /conversations/{id}, GET /conversations/{id}/messages |
conversations:write | POST /conversations, POST /conversations/{id}/messages (ingestion), POST /conversations/{id}/operator-messages, POST /conversations/{id}/takeover, /release, /close |
ai_seller:invoke | the AI reply in POST /conversations/{id}/messages (together with conversations:write) |
leads:read | GET /leads, GET /leads/{id}, GET /leads/by-external/{external_id} |
leads:write | POST /leads, PATCH /leads/{id}, POST /leads/{id}/accept, POST /leads/{id}/notes, POST /leads/{id}/tasks, POST /leads/{id}/actions/{action_id}/ack, PUT /leads/{id}/score, PUT /crm/status-map |
leads:score | POST /leads/{id}/score |
analytics:read | GET /analytics/timeseries, /analytics/traffic-sources, /analytics/funnel, /analytics/visibility |
analytics:write | POST /analytics/events, POST /analytics/conversions |
dashboards:read | GET /dashboards/overview, GET /dashboards/pulse |
articles:read | GET /articles, GET /articles/{id}, GET /articles/generation-jobs/{id} |
articles:write | PATCH /articles/{id}, POST /articles/{id}/approve, POST /articles/{id}/publish |
articles:generate | POST /articles/generation-jobs (or generate: true when approving a strategist topic) |
strategist:read | GET /strategist/reports, GET /strategist/reports/{id}, GET /strategist/topics, GET /strategist/actions, GET /strategist/actions/{id} |
strategist:write | POST /strategist/topics/{id}/approve, POST /strategist/topics/{id}/reject, POST /strategist/actions/{id}/confirm, POST /strategist/actions/{id}/cancel |
strategist:chat | POST /strategist/chat/sessions, POST /strategist/chat/sessions/{id}/turns |
webhooks:manage | GET/POST/PATCH/DELETE /webhooks, POST /webhooks/{id}/test, POST /webhooks/{id}/rotate-secret, GET /webhooks/{id}/deliveries |
| no scope | GET /me, GET /wallet, GET /operations/{id} |
#How the permissions of a request are computed
A request is allowed only if every condition is met. The effective permissions are an intersection:
request permissions = key scopes
∩ integration role
∩ CURRENT permissions of the owner employee
∩ license (api_access) and plan
| Layer | What it means in practice |
|---|---|
| Key scopes | Set at issuance. You cannot widen a scope; you issue a new key. |
| Integration role | The maximum of what the integration can do in the tenant at all. |
| Owner's current permissions | Checked at request time, not at issuance. If the employee loses a permission, the integration loses it too. If the employee is deactivated, the keys of their integrations are suspended and the tenant owner receives an alert. |
| License and plan | The api_access flag and plan restrictions. A refusal is 403 feature_not_in_plan. |
In addition, the tenant check applies: the tenant must be active. The identifier of another tenant's resource yields 404 not_found.
The consequence for an integrator: a key that worked yesterday can get scope_missing today if the owner's permissions changed. This is normal behavior; handle 403.
#IP allowlist
- Optional: a list of addresses or CIDR networks from which requests with the key are accepted.
- A request from any other address gets
403 ip_not_allowed. - For scopes with personal data (
conversations:*,leads:*) in the RU cell, the integration's IP list is mandatory; a key with such scopes is not issued without it. In addition, when connecting you specify the hosting country of the receiving system and the basis for the transfer; if the country is not Russia, theconversations:*andleads:*scopes are not granted (403 pii_transfer_not_allowed, Cells and data). The current list is visible inGET /me(key.ip_allowlist). - If you make calls from a cloud with floating addresses, pin the outbound address (a NAT gateway, a static IP).
- Using a key from a new address or a new country raises an alert to the tenant owner.
#Test keys
Status: Planned.
Keys with the test mode (sap_<cell>_test_…) work in the cell's sandbox, a separate tenant with fictitious data.
| Property | Test key |
|---|---|
| Data | fictitious; your production data is not affected |
| Paid operations | do not charge money |
| AI seller | replies with canned responses |
| Webhooks | events are delivered |
| Same code | the only difference is the key and the data |
A production key does not work in the sandbox, and a test key does not work in the production tenant.
#What to do if a key leaks
- Revoke the key in the dashboard (Settings → API). Revocation takes effect within 60 seconds at most.
- Issue a new key and update the secrets in your systems.
- Check the log "API usage" (retained for 90 days; it holds the method, route, status, time, cost, and
request_id, with no request bodies and no personal data) and the audit trail: what the key did after the suspected leak date. - If the key had scopes with personal data, check the personal data disclosure log (dashboard or
GET /disclosures, retained for 3 years) and assess which data may have been disclosed. The data controller's duties to notify regulators and data subjects are determined by the laws of your jurisdiction. - Check whether any new webhooks, keys, or integrations were created, and revoke the extras; if in doubt, rotate the webhook secrets.
- Remove the key from the repository history and from logs; a single deletion commit is not enough.
Prevention: minimal scopes, an IP allowlist, a lifetime shorter than the maximum, separate keys for separate systems, storage in a secrets manager.
#Authentication errors
| Code | HTTP | When |
|---|---|---|
invalid_api_key | 401 | The key is missing, wrong, revoked, or expired. The response is intentionally identical in all these cases |
wrong_cell | 401 | A key from another cell; the response has a correct_base_url field with the right address |
scope_missing | 403 | The key lacks the required scope, or it has been "cut down" by the role, the owner's permissions, or the plan |
ip_not_allowed | 403 | The address is outside the allowlist |
feature_not_in_plan | 403 | The plan or license does not include the API/feature |
pii_transfer_not_allowed | 403 | RU cell: a scope with personal data or include_pii was requested for an integration whose hosting country is outside Russia |
The response body is application/problem+json with a type of the form https://<cell domain>/developers/errors#<code> (Conventions). The full catalog is in Conventions.
#Notes
- Every write through the API goes into the tenant audit log with the integration and the key (
key_id), on behalf of the service account. - Brute force: keys are looked up by
key_idwith constant-time comparison; frequent failures from one address are throttled. - CORS is not supported: calling the API directly from a browser is impossible and must not be attempted.