> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keebai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PATCH /v1/crm/tickets/:id

> Partially update a ticket — close it, reassign it, retag it, or record its value.

Updates a ticket in place. Only the fields you send are written. This is also how you **close** a ticket and record whether it was won or lost.

To move a ticket between pipeline stages, use [`POST /v1/crm/tickets/:id/stage`](/dev/endpoints/crm-ticket-stage) instead — stage transitions run pipeline rules that a plain field write does not.

## Endpoint

```
PATCH https://api.keebai.com/v1/crm/tickets/{id}
```

## Required scope

`crm:tickets:write`

## Headers

| Header          | Required | Value                    |
| --------------- | -------- | ------------------------ |
| `Authorization` | Yes      | `Bearer kbai_pk_<token>` |
| `Content-Type`  | Yes      | `application/json`       |

## Path parameters

| Field | Type     | Required | Description               |
| ----- | -------- | -------- | ------------------------- |
| `id`  | `string` | Yes      | `ObjectId` of the ticket. |

## Body

All fields optional. Send only what changes.

| Field                  | Type       | Description                                                   |
| ---------------------- | ---------- | ------------------------------------------------------------- |
| `is_closed`            | `boolean`  | Closes or reopens the ticket.                                 |
| `resolution`           | `string`   | `won` or `lost`. Meaningful when closing.                     |
| `priority`             | `string`   | One of `low`, `medium`, `high`, `urgent`.                     |
| `category`             | `string`   | Free-text category.                                           |
| `assigned_to`          | `string`   | `ObjectId` of the new owner.                                  |
| `custom_fields`        | `object`   | Replaces the whole custom fields object.                      |
| `amount`               | `number`   | Monetary value of the opportunity.                            |
| `amount_currency`      | `string`   | ISO 4217 currency.                                            |
| `amount_status`        | `string`   | `estimated` or `confirmed`.                                   |
| `tag_ids`              | `string[]` | **Replaces** the whole tag list.                              |
| `add_tag_ids`          | `string[]` | Adds tags, leaving existing ones alone.                       |
| `remove_tag_ids`       | `string[]` | Removes tags, leaving the rest alone.                         |
| `conversation_summary` | `object`   | `{ motivo, estado, proximos_pasos, is_final }`, all optional. |

## Example request

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://api.keebai.com/v1/crm/tickets/65f4b5c6d7e8f9a0b1c2d3e4 \
    -H "Authorization: Bearer kbai_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "is_closed": true,
      "resolution": "won",
      "amount": 180000,
      "amount_status": "confirmed",
      "add_tag_ids": ["69f1a2b3c4d5e6f7a8b9c0d1"]
    }'
  ```

  ```js JavaScript theme={null}
  const response = await fetch(
    `https://api.keebai.com/v1/crm/tickets/${ticketId}`,
    {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${process.env.KEEBAI_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        is_closed: true,
        resolution: "won",
        amount_status: "confirmed",
      }),
    },
  );
  const result = await response.json();
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.patch(
      f"https://api.keebai.com/v1/crm/tickets/{ticket_id}",
      headers={"Authorization": f"Bearer {os.environ['KEEBAI_API_TOKEN']}"},
      json={
          "is_closed": True,
          "resolution": "won",
          "amount_status": "confirmed",
      },
      timeout=10,
  )
  resp.raise_for_status()
  result = resp.json()
  ```
</CodeGroup>

## Response

### 200 OK

```json theme={null}
{
  "ticket": {
    "_id": "65f4b5c6d7e8f9a0b1c2d3e4",
    "is_closed": true,
    "resolution": "won",
    "amount": 180000,
    "amount_currency": "CLP",
    "amount_status": "confirmed",
    "closed_at": "2026-05-02T16:10:22.114Z",
    "updated_at": "2026-05-02T16:10:22.114Z"
  },
  "just_closed": true,
  "action": "closed"
}
```

`just_closed` is `true` only on the request that flipped the ticket from open to closed, and `action` is `closed` on that same request and `updated` otherwise. Use `just_closed` to fire "deal won" side effects exactly once — a repeated `PATCH` with the same body returns `false`.

### 400 Bad Request

An enum value outside the allowed set, a malformed `ObjectId`, or a property the endpoint does not accept.

### 401 Unauthorized

Missing, invalid, revoked, or expired token.

### 403 Forbidden

The token does not have the `crm:tickets:write` scope.

### 404 Not Found

No ticket with that id, or the ticket belongs to a different company.

## Operational notes

* **Prefer `add_tag_ids` / `remove_tag_ids` over `tag_ids`.** The plural-only form replaces the list wholesale, so two systems tagging the same ticket will clobber each other.
* **`custom_fields` replaces too.** Read the ticket first if you only mean to set one key.
* Reopening a ticket is `{"is_closed": false}`. The previous `resolution` is not cleared automatically — send it explicitly if it no longer applies.


## OpenAPI

````yaml PATCH /v1/crm/tickets/{id}
openapi: 3.0.0
info:
  title: Keebai Public API
  description: Superficie pública REST autenticada con Personal Access Tokens (PAT).
  version: 1.0.0
  contact: {}
servers:
  - url: https://api.keebai.com
    description: Production
security: []
tags: []
paths:
  /v1/crm/tickets/{id}:
    patch:
      tags:
        - crm-tickets
      summary: Actualizar un ticket
      operationId: PublicTicketsController_update
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicTicketUpdateDto'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicTicketUpdateResponseDto'
      security:
        - PAT: []
components:
  schemas:
    PublicTicketUpdateDto:
      type: object
      properties:
        resolution:
          type: string
          enum:
            - won
            - lost
        is_closed:
          type: boolean
          description: Cierra o reabre el ticket
        priority:
          type: string
          enum:
            - low
            - medium
            - high
            - urgent
        category:
          type: string
        assigned_to:
          type: string
        custom_fields:
          type: object
        amount:
          type: number
        amount_currency:
          type: string
        amount_status:
          type: string
          enum:
            - estimated
            - confirmed
        tag_ids:
          description: Reemplaza la lista completa de tags
          type: array
          items:
            type: string
        add_tag_ids:
          description: Agrega tags sin tocar las existentes
          type: array
          items:
            type: string
        remove_tag_ids:
          description: Quita tags sin tocar las demás
          type: array
          items:
            type: string
        conversation_summary:
          $ref: '#/components/schemas/PublicConversationSummaryDto'
    PublicTicketUpdateResponseDto:
      type: object
      properties:
        ticket:
          $ref: '#/components/schemas/PublicTicketResponseDto'
        just_closed:
          type: boolean
          description: >-
            true sólo en la request que pasó el ticket de abierto a cerrado.
            Útil para disparar efectos una única vez.
        action:
          type: string
          description: closed | updated
      required:
        - ticket
        - just_closed
        - action
    PublicConversationSummaryDto:
      type: object
      properties:
        motivo:
          type: string
        estado:
          type: string
        proximos_pasos:
          type: string
        is_final:
          type: boolean
    PublicTicketResponseDto:
      type: object
      properties:
        _id:
          type: string
          description: ObjectId del ticket
        chat_user:
          type: string
          description: ObjectId del chat user (contacto)
        chat_user_name:
          type: string
        chat_user_avatar:
          type: string
        chat_user_channel:
          type: string
          description: Canal de origen (whatsapp, instagram…)
        chat_user_channel_id:
          type: string
        stage:
          description: Etapa actual del pipeline. Ausente si el ticket no tiene una.
          allOf:
            - $ref: '#/components/schemas/PublicTicketStageDto'
        is_closed:
          type: boolean
        resolution:
          type: string
          enum:
            - won
            - lost
        priority:
          type: string
          enum:
            - low
            - medium
            - high
            - urgent
        category:
          type: string
        assigned_to:
          $ref: '#/components/schemas/PublicTicketAssigneeDto'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/PublicTicketTagDto'
        custom_fields:
          type: object
        amount:
          type: number
        amount_currency:
          type: string
          description: Moneda ISO 4217
        amount_status:
          type: string
          enum:
            - estimated
            - confirmed
        conversation_summary:
          $ref: '#/components/schemas/PublicConversationSummaryDto'
        ai_feedback_status:
          type: string
          description: 'Feedback de la IA: resolved | unresolved'
        appointment_ids:
          description: Citas de scheduling vinculadas al ticket
          type: array
          items:
            type: string
        last_message:
          description: Último mensaje del contacto. Sólo presente en el listado.
          allOf:
            - $ref: '#/components/schemas/PublicTicketLastMessageDto'
        closed_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - _id
        - chat_user
        - is_closed
        - priority
        - tags
        - custom_fields
        - created_at
        - updated_at
    PublicTicketStageDto:
      type: object
      properties:
        stage_id:
          type: string
          description: Etapa de destino dentro del pipeline
        prioridad:
          type: string
          description: Prioridad reportada al mover la etapa
        contexto:
          type: string
          description: Contexto del movimiento
        problema:
          type: string
          description: Problema detectado
        recomendaciones:
          type: string
          description: Recomendaciones para el operador
      required:
        - stage_id
    PublicTicketAssigneeDto:
      type: object
      properties:
        id:
          type: string
          description: ObjectId del usuario
        full_name:
          type: string
        email:
          type: string
      required:
        - id
    PublicTicketTagDto:
      type: object
      properties:
        id:
          type: string
          description: ObjectId del tag
        name:
          type: string
        color:
          type: string
          description: 'Color hex, ej. #1890ff'
      required:
        - id
        - name
    PublicTicketLastMessageDto:
      type: object
      properties:
        id:
          type: string
        content:
          type: string
        role:
          type: string
        direction:
          type: string
          description: inbound | outbound
        created_at:
          type: string
          format: date-time
      required:
        - id
  securitySchemes:
    PAT:
      scheme: bearer
      bearerFormat: kbai_pk_<hex>
      type: http
      description: >-
        Personal Access Token con prefijo `kbai_pk_`. Generar desde el portal
        con permiso `developer.manage_tokens`.

````