AI agents¶
An AI agent talks to BottleCRM the same way any other program does: over the REST API under
/api/, authenticated with a personal access token. There is no separate agent protocol, no
agent-specific endpoint, and no service account. The agent is a normal API client, so RLS, RBAC
and field validation apply to it exactly as they apply to the web app and the mobile app.
This is a deliberate choice. BottleCRM previously shipped a Model Context Protocol (MCP) server at
/mcp, a thin proxy that re-issued each tool call as an HTTP request to this same API. It covered
eight entities out of the full product and added no capability the API did not already have, so it
was removed. Point your agent at the API directly and it can reach everything.
Give the agent a token¶
Mint a personal access token in the CRM at Settings → API tokens (/settings/api-tokens), or
via POST /api/profile/tokens/. The raw value is shown exactly once, at creation. Only a SHA-256
hash is stored, so a lost token cannot be recovered, only replaced.
Every request carries it as a bearer token:
See Tokens and API keys for the full lifecycle: creation, expiry, validation and revocation.
Let the agent discover the API¶
The backend publishes an OpenAPI 3 schema generated by drf-spectacular. Feed it to the agent and it can work out the endpoints, request bodies, enums and required fields on its own, without you hand-maintaining a tool registry that drifts from the real URLconf:
| What | Where |
|---|---|
| Machine-readable schema | GET /schema/ |
| Interactive browser docs | /swagger-ui/ |
| ReDoc rendering | /api/schema/redoc/ |
| Hand-written endpoint list | Endpoint index |
Request and response shapes, pagination, filtering and error envelopes are described in API conventions and Errors.
Permissions¶
The agent is exactly as privileged as the token it holds, no more and no less.
PATAuthentication (backend/common/pat_auth.py) resolves the token to its owning Profile and
sets request.profile and request.org from it, the same code path every other PAT-authenticated
request goes through. The agent inherits that user's role, org and RLS scope.
A token does carry a scopes field, but nothing enforces it yet. There is currently no way to
issue an agent a token narrower than its owner's own access. Treat every token you configure into
an agent as equivalent to handing that agent your login for as long as the token is valid, and
give it to a profile whose role matches the job you want done.
A token stops working the moment its owning profile or org is deactivated: resolve_valid_pat
rejects it with "Token owner or org is inactive". That makes a deactivated user's token dormant
rather than live, though it is still worth revoking on offboarding, because reactivating the
account revives the token.
Revoking¶
To cut off an agent immediately, revoke its token. Which endpoint you use depends on your role:
- Any role, own token:
DELETE /api/profile/tokens/{id}/. Self-service, scoped to your own tokens. Another user's token id returns 404 rather than being revoked. - Admins, any token in the org:
DELETE /api/org/tokens/{id}/, backed by the org-wide oversight list atGET /api/org/tokens/. This is what the Settings → API tokens page calls. A non-admin who opens that page sees "Admins only", not a token list.
Revoke is idempotent: a second call on an already-revoked token is a no-op 200.
Practical notes¶
A few API details that commonly trip up a first agent integration:
- Comments are not a sub-action. Every module routes them at
comment/<id>/, for examplePOST /api/leads/comment/<id>/, not/api/leads/<id>/add_comment/. - Converting a lead is a status change, not an action endpoint:
PATCH /api/leads/<id>/with{"status": "converted"}. - Solutions live under the cases app at
/api/cases/solutions/, not at a top-level/api/solutions/. InvoiceDetailViewimplements onlygetandput. APATCHorDELETEagainst/api/invoices/<id>/returns 405. Use a fullPUT, orPOST /api/invoices/<id>/send/to send one. See Invoices and estimates.- Sending an invoice emails a customer. If you let an agent call it, put a confirmation step in front of it. The API will not ask twice.
Worked examples in two languages: Python and JavaScript.