Skip to main content
The CRM surface exposes the core entities of Keebai’s customer relationship data — customers, tickets, tasks, and notes — as a plain REST API under /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.
Customers and tickets are separate records that are not linked by a foreign key. A customer is the commercial identity; a ticket is a unit of work tied to a contact — the record created when someone writes to one of your channels, exposed as /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.
Reads are company-wide, writes are project-scoped.Records you create are stamped with the project your token belongs to. List endpoints, however, return every record in the company, including records that belong to other projects. A token minted for project A will see tickets, customers, notes, and tasks created under project B.If your company uses more than one project and you need per-project separation in the results, filter client-side on the project field of each record. Tasks have no project field at all — they are company-level by design.

Identifiers

All ids are MongoDB ObjectIds: 24 lowercase hexadecimal characters, for example 65a1f2b3c4d5e6f7a8b9c0d1. 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 take limit 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 a PATCH 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_ids replaces the entire tag list.
  • add_tag_ids and remove_tag_ids modify the list incrementally, leaving the tags you did not mention alone.
Use the incremental pair whenever more than one system writes tags to the same ticket.

Custom fields

Tickets and customers both carry a custom_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.
custom_fields is merged, not replaced. A PATCH carrying {"custom_fields": {"a": 1}} leaves every other key alone. There is no way to delete a key through the API — set it to null.Values are not type-checked on ticket and customer writes. Sending a string into a number field stores the string. The one path that does validate is the event ingest, which rejects the whole request rather than storing a bad value.

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 is POST /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.