> ## 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.

# PUT /v1/functions/{id}

> Update a webhook function. Omitting headers keeps the stored ones; sending them replaces the whole map.

Partial update: only the fields present in the body change.

The subtlety is inside `context`. Because [header and query-string values are masked on read](/dev/assistants/webhook-security#secrets-are-write-only), you cannot round-trip them, so this endpoint merges instead.

| What you send               | What happens                                       |
| --------------------------- | -------------------------------------------------- |
| No `context`                | Nothing about the request changes                  |
| `context` without `headers` | The stored headers are kept                        |
| `context` with `headers`    | The whole map is replaced with what you sent       |
| A value equal to `***`      | `400 FUNCTION_MASKED_VALUE_WRITE`, naming the keys |

The last row exists so the obvious client pattern — `GET`, change one field, `PUT` it all back — fails loudly instead of writing the literal string `***` over a live credential.

## Endpoint

```
PUT https://api.keebai.com/v1/functions/{id}
```

## Required scope

`assistants:write`

## Headers

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

## Path parameters

| Parameter | Type     | Description                 |
| --------- | -------- | --------------------------- |
| `id`      | `string` | `ObjectId` of the function. |

## Body

Every field is optional. `context`, when present, must carry at least `url`.

| Field          | Type       | Description                                                                                  |
| -------------- | ---------- | -------------------------------------------------------------------------------------------- |
| `name`         | `string`   | Machine name, unique per company.                                                            |
| `display_name` | `string`   | Up to 200 characters.                                                                        |
| `description`  | `string`   | Up to 1000 characters.                                                                       |
| `context`      | `object`   | Same shape as [on create](/dev/endpoints/functions-create#body). Merged per the table above. |
| `tool_params`  | `object[]` | Replaces the whole list.                                                                     |
| `tags`         | `string[]` | Replaces the whole list.                                                                     |
| `is_active`    | `boolean`  | `false` stops the model being offered the function.                                          |

## Example request

<CodeGroup>
  ```bash curl theme={null}
  # Change the URL, keep the stored credentials.
  curl -X PUT https://api.keebai.com/v1/functions/6650cccc3333dddd4444eeee \
    -H "Authorization: Bearer kbai_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "context": {
        "url": "https://erp.example.com/v2/stock/{{sku}}",
        "method": "GET",
        "timeout": 10
      }
    }'
  ```

  ```js JavaScript theme={null}
  const url = "https://api.keebai.com/v1/functions/6650cccc3333dddd4444eeee";
  const headers = {
    Authorization: `Bearer ${process.env.KEEBAI_API_TOKEN}`,
    "Content-Type": "application/json",
  };

  // Rotating a credential: send the new value explicitly.
  await fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify({
      context: {
        url: "https://erp.example.com/v2/stock/{{sku}}",
        method: "GET",
        headers: { Authorization: `Bearer ${process.env.ERP_TOKEN_NEW}` },
      },
    }),
  });

  // Switching it off without changing anything else.
  await fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify({ is_active: false }),
  });
  ```

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

  resp = requests.put(
      "https://api.keebai.com/v1/functions/6650cccc3333dddd4444eeee",
      headers={"Authorization": f"Bearer {os.environ['KEEBAI_API_TOKEN']}"},
      json={
          "context": {
              "url": "https://erp.example.com/v2/stock/{{sku}}",
              "method": "GET",
              "timeout": 10,
          }
      },
      timeout=10,
  )
  if resp.status_code == 400 and resp.json()["error"]["code"] == "FUNCTION_MASKED_VALUE_WRITE":
      ...  # you echoed a *** back; send the real value or omit the field
  resp.raise_for_status()
  ```
</CodeGroup>

## Response

### 200 OK

The function, same shape as [`GET /v1/functions/{id}`](/dev/endpoints/functions-get).

### 400 Bad Request

A field over its limit or an unknown property, plus three specific codes:

* `FUNCTION_MASKED_VALUE_WRITE` — a `headers` or `params` value was the literal `***`. `details.field` and `details.keys` name them.
* `FUNCTION_URL_NOT_ALLOWED` — the URL failed validation. `details.reason` names the rule.
* `FUNCTION_TIMEOUT_NOT_ALLOWED` — `timeout` outside 1–30 seconds.

### 401 Unauthorized

Missing, invalid, revoked, or expired token.

### 403 Forbidden

The token does not have the `assistants:write` scope.

### 404 Not Found

No webhook function with that id in the token's company.

### 409 Conflict

The new `name` is already taken by another function in the company.

### 502 Bad Gateway

The assistant service is unreachable.

## Operational notes

* **`context` is replace-within-merge.** Fields you omit inside `context` — `method`, `timeout`, `body`, `response_path` — go back to their defaults, not to their stored values. Only `headers` and `params` are preserved on omission, because they are the ones you cannot read back. Send the whole `context` you want, minus the secrets.
* **Updates are read-modify-write, so concurrent writers lose each other.** Merging the stored secrets requires reading them first; two overlapping updates to one function leave whichever finished last. Serialise them.
* **The URL is only re-validated when you send `context`.** A function created before a validation rule existed keeps working until you next touch its URL.
* **Renaming breaks nothing immediately but changes what the model calls.** Blocks reference the function by id, not by name, so the binding survives — but any prompt text mentioning the old name is now wrong.
* **`tool_params` and `tags` replace, they do not append.** Read the current values first if you mean to add one.


## OpenAPI

````yaml PUT /v1/functions/{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/functions/{id}:
    put:
      tags:
        - functions
      summary: Actualizar una funcion de webhook
      description: >-
        Actualiza solo los campos presentes. Dentro de context, omitir headers o
        params preserva los guardados y mandarlos los reemplaza enteros.
        Devolver el '***' de una lectura da 400 en vez de pisar la credencial.
      operationId: PublicFunctionsController_update
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicFunctionUpdateDto'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFunctionEnvelopeDto'
      security:
        - PAT: []
components:
  schemas:
    PublicFunctionUpdateDto:
      type: object
      properties:
        name:
          type: string
          maxLength: 100
        display_name:
          type: string
          maxLength: 200
        description:
          type: string
          maxLength: 1000
        context:
          $ref: '#/components/schemas/PublicWebhookContextDto'
        tool_params:
          maxItems: 30
          type: array
          items:
            $ref: '#/components/schemas/PublicFunctionParamDto'
        tags:
          maxItems: 20
          type: array
          items:
            type: string
        is_active:
          type: boolean
    PublicFunctionEnvelopeDto:
      type: object
      properties:
        function:
          $ref: '#/components/schemas/PublicFunctionDto'
      required:
        - function
    PublicWebhookContextDto:
      type: object
      properties:
        url:
          type: string
          description: >-
            URL https publica. Se rechazan loopback, rangos privados, hosts
            internos del cluster y placeholders en el host.
          example: https://erp.example.com/stock
        method:
          type: string
          enum:
            - GET
            - POST
            - PUT
            - PATCH
            - DELETE
          default: POST
        headers:
          type: object
          description: >-
            Headers de la request. Los valores son de solo escritura: en lectura
            vuelven enmascarados. Omitir el campo en un update preserva los
            guardados; mandarlo los reemplaza completos.
          example:
            Authorization: Bearer sk_live_xxx
        params:
          type: object
          description: >-
            Query string de la request. Mismo trato que headers: enmascarado en
            lectura.
        body:
          type: object
          description: >-
            Body de la request. Acepta placeholders '{{arg}}' que se reemplazan
            con los argumentos del modelo.
          example:
            sku: '{{sku}}'
        timeout:
          type: number
          default: 30
          minimum: 1
          maximum: 30
          description: Segundos de espera antes de abortar la llamada.
        response_path:
          type: string
          maxLength: 200
          description: >-
            Ruta dentro del JSON de respuesta a extraer, por ejemplo
            'data.items'. Sin esto se entrega la respuesta completa.
      required:
        - url
    PublicFunctionParamDto:
      type: object
      properties:
        name:
          type: string
          maxLength: 100
          description: Nombre del argumento tal como lo va a pasar el modelo.
        description:
          type: string
          maxLength: 500
          description: >-
            Que significa el argumento. El modelo lee esto para decidir que
            mandar.
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - array
            - object
          default: string
        required:
          type: boolean
          default: true
      required:
        - name
    PublicFunctionDto:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        display_name:
          type: string
        description:
          type: string
        type:
          type: string
          example: webhook
        tool_params:
          type: array
          items:
            $ref: '#/components/schemas/PublicFunctionParamResponseDto'
        context:
          $ref: '#/components/schemas/PublicWebhookContextResponseDto'
        tags:
          type: array
          items:
            type: string
        is_active:
          type: boolean
        used_by_blocks:
          description: >-
            Bloques que referencian esta funcion. Mientras la lista no este
            vacia, borrarla devuelve 409.
          type: array
          items:
            $ref: '#/components/schemas/PublicFunctionUsageDto'
        created_at:
          type: string
        updated_at:
          type: string
      required:
        - _id
        - name
        - display_name
        - description
        - type
        - tool_params
        - context
        - tags
        - is_active
        - used_by_blocks
        - created_at
        - updated_at
    PublicFunctionParamResponseDto:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - array
            - object
        required:
          type: boolean
      required:
        - name
        - type
        - required
    PublicWebhookContextResponseDto:
      type: object
      properties:
        url:
          type: string
        method:
          type: string
          enum:
            - GET
            - POST
            - PUT
            - PATCH
            - DELETE
        headers:
          type: object
          additionalProperties:
            type: string
          description: >-
            Headers configurados. Las claves se muestran y los valores vuelven
            siempre como '***'.
          example:
            Authorization: '***'
        params:
          type: object
          additionalProperties:
            type: string
          description: >-
            Query string configurada, con los valores enmascarados igual que
            headers.
        body:
          type: object
          additionalProperties: true
          description: >-
            Body de la request. No se enmascara: es la plantilla que rellena el
            modelo.
        timeout:
          type: number
        response_path:
          type: string
      required:
        - url
        - method
        - headers
        - params
        - timeout
    PublicFunctionUsageDto:
      type: object
      properties:
        block_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum:
            - personification
            - objective
            - response_format
            - steps
            - cases_possible
            - do_not
            - product_info
            - store_info
            - reservation_info
            - tables_info
            - tags
            - response_policy
      required:
        - block_id
        - name
        - type
  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`.

````