/v1/crm, plus an event ingest that records what happens in your own systems onto a contact’s timeline. Every endpoint authenticates with a Personal Access Token and is gated by its own scope.
Data model
Customers
A person or organization you do business with. Identified by name plus optional email, phone, and a national identification number that is unique within your company.
Tickets
A conversation, deal, or case moving through a pipeline. Always attached to a chat user (the contact behind a messaging conversation), and optionally to a stage, an owner, tags, and a monetary amount.
Tasks
A unit of work with a title, an optional assignee, a status, a priority, and a due date. Tasks stand on their own — they are not attached to a ticket or a customer.
Notes
A free-text annotation, optionally typed as a call, email, or meeting, and optionally attached to a chat user or a ticket.
Events
Something that happened to a contact, appended to their ticket’s timeline. Unlike the four above, events are append-only and their payload is validated against the fields their type declares.
Custom fields
Tickets and customers both carry a
custom_fields object. The same field definitions compose the payload of an event type, which is how an event’s values end up on the ticket./v1/contacts and referred to as chat_user throughout the CRM. If you need to correlate a customer with a ticket, match on phone or email, or store the customer id in the ticket’s custom_fields.
Scopes
The
delete scopes are split out on purpose: a sync job that keeps records in step with an external system should be able to create and update without being able to destroy.
crm:events:write deserves its own token. It is the only CRM scope an external system needs to push history in, and it writes to tickets and contacts as a side effect — keeping it separate from your read tokens means you can revoke the ingest without blinding your dashboards.
Tenancy is implicit
Every CRM record belongs to a company, and most also belong to a project. You never send either one. Keebai derives them from the token on each request:
Sending any of these in the request body returns
400 Bad Request — the public API rejects unknown properties rather than silently ignoring them. This is deliberate: it makes an attempt to write into another tenant fail loudly instead of quietly doing nothing.
Identifiers
All ids are MongoDB ObjectIds: 24 lowercase hexadecimal characters, for example65a1f2b3c4d5e6f7a8b9c0d1. That includes the ids you pass in request bodies — chat_user, assigned_to, stage_id, ticket_id, and the entries of tags / tag_ids.
The chat_user id comes from GET /v1/contacts, which is the supported way to look a contact up by phone or name before creating a ticket or a note.
There is still no public endpoint for discovering pipelines, stages, tags, or users. Read those ids from the Keebai portal, or capture them from the payloads of the outbound webhooks your integration already receives.
Pagination
List endpoints takelimit and offset and return a consistent envelope:
limit caps at 200 on every list endpoint, and a value above it returns 400 — page with offset rather than trying to raise the ceiling. The default page size is 50 everywhere except notes, which defaults to 100.
Updates are partial
Every update endpoint is aPATCH and applies only the fields you send. Omitted fields keep their current value; there is no PUT-style full replacement.
Two fields on tickets are worth calling out, because they behave differently from the rest:
tag_idsreplaces the entire tag list.add_tag_idsandremove_tag_idsmodify the list incrementally, leaving the tags you did not mention alone.
Custom fields
Tickets and customers both carry acustom_fields object — a flat map keyed by the field’s key, holding whatever your company configured in the portal under CRM → Fields.
There is no public endpoint for reading the field definitions, so an integration has to know the keys it writes. Read them from the portal once and treat them as part of your contract.
Writes have side effects
CRM writes are not inert. Each one can trigger work you should know about before you build a sync loop:Beware of loops. If your integration both subscribes to
crm.ticket.updated and writes tickets in response, an event ingest that touches a ticket field will call you back. Guard with an idempotency check on your side, or filter out changes your own token produced.Rate limits
The CRM endpoints draw from the shared per-project quota — 60 a minute, 200 an hour, 1,000 a day across every endpoint of the public API. The one exception isPOST /v1/webhook/events, which carries a far higher allowance of its own (300 a minute, 50,000 a day) because an ingest is not interactive traffic.
If you are syncing a CRM with tens of thousands of records, the daily cap is the binding constraint. Prefer outbound webhooks over polling, and email support before you start rather than after the first 429.
Errors
The CRM surface uses the same error envelope as the rest of the public API:
Branch on
error.code, never on error.message — the wording is not stable and some of it is in Spanish.
Next steps
Create a customer
Register the commercial identity you will bill and contact.
Open a ticket
Start a case in a pipeline and move it through its stages.
Emit an event
Push what happened in your ERP, POS, or backoffice onto the contact’s timeline.
Subscribe to changes
Get
crm.ticket.* delivered to your server instead of polling for them.