{
  "openapi": "3.1.0",
  "info": {
    "title": "DialSprint API",
    "version": "1.0.0",
    "description": "API para que tus sistemas (CRM, ERP, core del negocio) creen, actualicen e\nimporten contactos en DialSprint.\n\n**Autenticación.** Una clave de cuenta en el encabezado `Authorization`.\nLa clave pertenece a la empresa, no a la persona que la creó, así que\nsigue andando cuando esa persona se va. No se manda ningún otro encabezado\nde identificación: la clave ES la cuenta.\n\n**Requiere una cuenta activa.** La cuenta se activa con la primera carga de saldo. No hay entorno de prueba: se integra\ncontra la cuenta real, y por eso un teléfono repetido responde 409 en vez\nde crear un duplicado.\n\n**Reintentos seguros.** Mandá `Idempotency-Key: <algo único>` en cualquier\nescritura y un reintento con la misma clave devuelve la respuesta de la\nprimera, sin escribir de nuevo. La clave se recuerda 24 horas; si la\nreutilizás con otro cuerpo, te avisamos en vez de adivinar.\n\n**Avisos.** En vez de preguntar cada minuto, registrá un webhook en\nConfiguración → API y te avisamos apenas pasa algo (ver `webhooks` al\nfinal de esta referencia).\n\n**Compatibilidad.** Los campos nuevos en una respuesta, los valores nuevos\nde un enum y los parámetros opcionales nuevos son aditivos y NO cambian la\nversión. Un cambio que rompa algo estrena `/v2` y `/v1` sigue funcionando.\n",
    "contact": {
      "name": "DialSprint",
      "url": "https://dialsprint.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.dialsprint.com/api/v1",
      "description": "Producción"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Contactos",
      "description": "Crear, leer y actualizar contactos de a uno."
    },
    {
      "name": "Importación",
      "description": "Cargar contactos de a miles."
    },
    {
      "name": "Llamadas",
      "description": "Qué pasó después de que el contacto entró."
    },
    {
      "name": "Cumplimiento",
      "description": "Los números que no se llaman."
    },
    {
      "name": "Campañas",
      "description": "Qué se está discando, y a quién sumarle."
    },
    {
      "name": "Conversaciones",
      "description": "Los hilos de texto, de sólo lectura."
    },
    {
      "name": "Recontactos",
      "description": "Los compromisos que tomaron los agentes."
    },
    {
      "name": "Catálogo",
      "description": "Los campos y las listas contra los que se escribe."
    }
  ],
  "paths": {
    "/contacts": {
      "get": {
        "tags": [
          "Contactos"
        ],
        "summary": "Listar contactos",
        "description": "Paginado por cursor y ordenado del más nuevo al más viejo. Usá\n`updated_since` para traer sólo lo que cambió desde tu última corrida.\n",
        "parameters": [
          {
            "name": "external_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tu propio identificador."
          },
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Un teléfono en cualquier formato. Se normaliza antes de buscar."
          },
          {
            "name": "list_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Contactos de una lista."
          },
          {
            "name": "updated_since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "per_page": {
                          "type": "integer"
                        },
                        "next_cursor": {
                          "type": "string",
                          "nullable": true
                        },
                        "prev_cursor": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Contactos"
        ],
        "summary": "Crear un contacto",
        "description": "Requiere `name` y al menos un teléfono. Si alguno de los teléfonos ya\npertenece a otro contacto responde **409** con el `contact_id` del\ndueño, así la recuperación es un solo PATCH.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactWrite"
              },
              "example": {
                "external_id": "crm-8837",
                "name": "Juan Pérez",
                "phones": [
                  "+5491155550000"
                ],
                "email": "juan@ejemplo.com",
                "timezone": "America/Argentina/Buenos_Aires",
                "tags": [
                  "plan-30"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/contacts/{id}": {
      "get": {
        "tags": [
          "Contactos"
        ],
        "summary": "Traer un contacto",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Contactos"
        ],
        "summary": "Actualizar un contacto (merge)",
        "description": "**Es un merge, no un reemplazo.** Un campo que no mandás queda como\nestaba. Un campo simple con `null` se borra.\n\nLas listas (`phones`, `tags`, campos personalizados de varios valores)\nson **aditivas**: lo que mandás se SUMA. Para sacar algo, usá\n`remove_phones`, `remove_tags` o `remove_fields`. Es a propósito: los\nagentes cargan teléfonos y etiquetas desde la app todos los días, y una\nsincronización que reemplace la lista borraría el teléfono que un agente\nconsiguió ayer.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactPatch"
              },
              "example": {
                "company": "Ejemplo SRL",
                "tags": [
                  "interesado"
                ],
                "remove_tags": [
                  "plan-30"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        }
      }
    },
    "/contacts/by-external-id/{external_id}": {
      "put": {
        "tags": [
          "Contactos"
        ],
        "summary": "Crear o reemplazar por tu identificador",
        "description": "La operación idempotente: se puede reintentar sin miedo. Responde\n**201** cuando crea y **200** cuando actualiza, siempre con el contacto\nen el cuerpo.\n\nA diferencia de PATCH, **PUT reemplaza**: es el verbo que significa\n\"este es el registro completo\". Si tu identificador apunta a un contacto\ny alguno de los teléfonos pertenece a otro, responde 409 con los dos ids\ny no escribe nada: un teléfono nunca se muda de contacto solo.\n",
        "parameters": [
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Creado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        }
      }
    },
    "/contact-imports": {
      "post": {
        "tags": [
          "Importación"
        ],
        "summary": "Importar contactos en lote",
        "description": "Hasta 5.000 contactos por pedido (5 MB en total, 100 KB por fila).\nResponde **202** con el trabajo: se procesa en segundo plano y el estado\nse consulta aparte.\n\nUn contacto cuyo teléfono esté en una lista de supresión **se crea\nigual** y queda contado en `suppressed`. El bloqueo ocurre al momento de\ndiscar, no al momento de importar.\n\nCada fila se identifica por `external_id` y, si no lo trae o no existe,\npor sus teléfonos. Lo que pasa con un contacto que ya existe lo decide\n`existing_contacts`: `update` (por defecto) lo actualiza con la fila;\n`keep` lo deja como está y sólo lo suma a `contact_list_id`, y queda\ncontado en `unchanged`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contacts"
                ],
                "properties": {
                  "contacts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5000,
                    "items": {
                      "$ref": "#/components/schemas/ContactWrite"
                    }
                  },
                  "contact_list_id": {
                    "type": "string",
                    "nullable": true
                  },
                  "existing_contacts": {
                    "type": "string",
                    "enum": [
                      "update",
                      "keep"
                    ],
                    "default": "update",
                    "description": "Qué hacer con un contacto que ya existe. `update` le aplica los\nvalores de la fila y le suma los números nuevos (una celda vacía\nnunca borra un dato). `keep` no le cambia nada (ni campos, ni\nteléfonos, ni zona horaria, ni `external_id`) y sólo lo suma a\nla lista.\n"
                  },
                  "default_timezone": {
                    "type": "string",
                    "nullable": true,
                    "description": "Zona IANA para las filas que no traen una."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aceptado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Import"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/contact-imports/{id}": {
      "get": {
        "tags": [
          "Importación"
        ],
        "summary": "Estado de una importación",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Import"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/contact-imports/{id}/errors": {
      "get": {
        "tags": [
          "Importación"
        ],
        "summary": "Filas que fallaron",
        "description": "Muestra una MUESTRA de las filas con error, no todas: los contadores del\ntrabajo son exactos y `truncated` avisa cuando la lista se quedó corta.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "row": {
                                "type": "integer"
                              },
                              "code": {
                                "type": "string"
                              },
                              "field": {
                                "type": "string",
                                "nullable": true
                              },
                              "detail": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "total": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "limit": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "La importación todavía está corriendo."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/calls": {
      "get": {
        "tags": [
          "Llamadas"
        ],
        "summary": "Listar llamadas",
        "description": "Una fila por CONVERSACIÓN, no por pata. Internamente una transferencia\ngenera una fila nueva y un participante de conferencia otra; acá se\nreporta la raíz, que es lo que un CRM quiere pegarle a una cuenta.\n\nEl audio de las grabaciones NO se expone: los links del producto son\nURLs firmadas de 12 h hechas para su reproductor, y una de esas en los\nlogs de un cliente es una conversación reproducible. `has_recording`\ncontesta si hay audio sin entregarlo.\n",
        "parameters": [
          {
            "name": "contact_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "inbound",
                "outbound"
              ]
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "answered",
                "no_answer",
                "busy",
                "voicemail",
                "failed",
                "abandoned",
                "no_verdict",
                "canceled",
                "provider_error"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Call"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "per_page": {
                          "type": "integer"
                        },
                        "next_cursor": {
                          "type": "string",
                          "nullable": true
                        },
                        "prev_cursor": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/calls/{id}": {
      "get": {
        "tags": [
          "Llamadas"
        ],
        "summary": "Traer una llamada",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Call"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/dispositions": {
      "get": {
        "tags": [
          "Llamadas"
        ],
        "summary": "Catálogo de tipificaciones",
        "description": "Traelo una vez para armar tu mapeo antes de la primera llamada, en vez\nde descubrir el vocabulario fila por fila. `category` y `action` son lo\nque realmente mueve la gestión del contacto, no decoración. Las\ntipificaciones archivadas no aparecen: ya no se usan en llamadas\nnuevas, y las llamadas que ya tipificaron las siguen nombrando.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "action": {
                            "type": "string"
                          },
                          "outcome": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_active": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/suppression-lists": {
      "get": {
        "tags": [
          "Cumplimiento"
        ],
        "summary": "Listar listas de supresión",
        "description": "`status` importa: una lista todavía procesando una carga no está\nfiltrando todo lo que va a filtrar.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "national_dnc",
                              "sector_dnc",
                              "custom"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "ready",
                              "processing",
                              "failed"
                            ]
                          },
                          "entries_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    },
    "/suppression-lists/{id}/entries": {
      "post": {
        "tags": [
          "Cumplimiento"
        ],
        "summary": "Agregar números a una lista",
        "description": "La única escritura de cumplimiento que acepta la API, y está acá porque\nmuchas veces el sistema del cliente es donde primero se entera de una\nbaja: alguien se lo dice a un agente por otro canal, llega una carta.\n\nEs idempotente: el índice único (lista, teléfono) hace que reenviar el\nlote de ayer no agregue nada. Un número inválido se REPORTA y no voltea\nel lote entero.\n\nNo se escribe sobre el contacto: `dnc_at` pasa por el servicio que\nescribe la fila de auditoría, y lo que se suprime es el número.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phones"
                ],
                "properties": {
                  "phones": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "phones": [
                  "+5491155550000",
                  "11 5555 0001"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "added": {
                          "type": "integer"
                        },
                        "already_present": {
                          "type": "integer"
                        },
                        "invalid": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "entries_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        }
      }
    },
    "/contact-fields": {
      "get": {
        "tags": [
          "Catálogo"
        ],
        "summary": "Campos de contacto",
        "description": "Traelo una vez y guardate el mapa. Los seis campos del sistema tienen un\nnombre estable (`internal_name`) y van como clave de primer nivel; los\ncampos personalizados se direccionan por su `id` dentro de `fields`.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Field"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/contact-lists": {
      "post": {
        "tags": [
          "Catálogo"
        ],
        "summary": "Crear una lista",
        "description": "Para el que carga una tanda por día y no quiere entrar al panel a crear\nla lista antes. Crea una lista común: los segmentos arman sus miembros\ndesde un filtro y se editan en la app.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "timezone": {
                    "type": "string",
                    "nullable": true,
                    "description": "Identificador IANA."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ContactList"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        }
      },
      "get": {
        "tags": [
          "Catálogo"
        ],
        "summary": "Listas de contactos",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "is_segment": {
                            "type": "boolean",
                            "description": "Una lista segmentada arma sus miembros sola; no se le agregan contactos."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/contacts/{id}/notes": {
      "get": {
        "tags": [
          "Contactos"
        ],
        "summary": "Notas del contacto",
        "description": "El registro que ve el agente antes de discar. Trae lo que escribieron\nlos agentes al tipificar y lo que escribió tu propio sistema.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Note"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Contactos"
        ],
        "summary": "Dejar una nota",
        "description": "La nota aparece en la ficha que el agente mira antes de la próxima\nllamada. El autor es la clave, no una persona.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Note"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        }
      }
    },
    "/contacts/{id}/archive": {
      "post": {
        "tags": [
          "Contactos"
        ],
        "summary": "Archivar un contacto",
        "description": "Lo saca de todo camino de discado y lo deja legible. **No hay borrado**,\ny la razón es concreta: al borrar un contacto se van sus teléfonos, y\ncon ellos la baja de SMS que había pedido el cliente. Un sistema que\nborra y recrea para sincronizar resucitaría esa baja.\n\nEs idempotente, así que podés reenviar tu cola sin pensarlo. El tope es\nde 500 por día y por cuenta.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archivado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Contactos"
        ],
        "summary": "Desarchivar",
        "description": "Explícito a propósito, porque un PUT que toque un contacto archivado lo\ndeja archivado.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Desarchivado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "tags": [
          "Campañas"
        ],
        "summary": "Listar campañas",
        "description": "El catálogo al que apunta el `campaign_id` del registro de llamadas.\n`accepts_enrollment` te dice de antemano si sumarle contactos va a\nfuncionar.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "paused",
                "archived"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "get": {
        "tags": [
          "Campañas"
        ],
        "summary": "Traer una campaña",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/campaigns/{id}/contacts": {
      "post": {
        "tags": [
          "Campañas"
        ],
        "summary": "Sumar contactos a una campaña",
        "description": "\"Este contacto entró a la campaña, empezá a llamarlo\". Los contactos ya tienen\nque existir: creálos primero y mandá acá sus `id`.\n\nReenviar la misma tanda no suma nada, así que un reintento es seguro.\nUn contacto archivado se informa en `skipped_archived` en vez de\ndesaparecer sin decir nada.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contact_ids"
                ],
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "added": {
                          "type": "integer"
                        },
                        "already_in_list": {
                          "type": "integer"
                        },
                        "skipped_archived": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "La campaña está archivada (`campaign_archived`), no tiene lista\nactiva (`campaign_without_list`) o sólo se alimenta de segmentos\n(`list_is_segment`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/conversations": {
      "get": {
        "tags": [
          "Conversaciones"
        ],
        "summary": "Listar hilos",
        "description": "Los hilos de texto, de sólo lectura. El hilo es (línea, contacto): la\nbandeja dice quién lo trabaja y puede cambiar.\n",
        "parameters": [
          {
            "name": "contact_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed"
              ]
            }
          },
          {
            "name": "unanswered",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "La misma cola \"sin responder\" que ve el agente."
          },
          {
            "name": "updated_since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Conversation"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/conversations/{id}": {
      "get": {
        "tags": [
          "Conversaciones"
        ],
        "summary": "Traer un hilo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Conversation"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/conversations/{id}/items": {
      "get": {
        "tags": [
          "Conversaciones"
        ],
        "summary": "Mensajes de un hilo",
        "description": "Mensajes y llamadas, del más nuevo al más viejo. Las notas internas\n(lo que los agentes se dicen entre ellos) no se publican.\n\nDe los archivos adjuntos se describe el nombre, el tipo y el tamaño; el\narchivo no se entrega, igual que el audio de las grabaciones.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/callbacks": {
      "get": {
        "tags": [
          "Recontactos"
        ],
        "summary": "Listar recontactos",
        "description": "Lo que el agente se comprometió a hacer, por ejemplo volver a llamar\no enviar un presupuesto, con su fecha.\n",
        "parameters": [
          {
            "name": "contact_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "completed",
                "cancelled",
                "expired"
              ]
            }
          },
          {
            "name": "due_from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "due_to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Callback"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    }
  },
  "webhooks": {
    "contact.created": {
      "post": {
        "summary": "Contacto creado",
        "description": "Alguien cargó un contacto, en el panel o por la API. Una carga masiva NO dispara uno por fila.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Devolvé 2xx apenas lo recibas. Si tardás más de 10 segundos",
            "reintentamos.": null
          }
        }
      }
    },
    "contact.updated": {
      "post": {
        "summary": "Contacto modificado",
        "description": "Cambió algo de la ficha, incluido el bloque `compliance`: así te\nenterás de un \"no llamar\" que la API no te deja escribir.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "contact.archived": {
      "post": {
        "summary": "Contacto archivado",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "call.completed": {
      "post": {
        "summary": "Llamada terminada",
        "description": "Una llamada llegó a su fin, con su resultado. Una sola por conversación:\nlas patas de una transferencia no son llamadas nuevas.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "call.wrapped_up": {
      "post": {
        "summary": "Llamada tipificada",
        "description": "El agente cargó la tipificación y las notas. Este es el\nevento que importa: \"contactado, confirma el 12\".\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "disposition.applied": {
      "post": {
        "summary": "Tipificación con automatización",
        "description": "El agente eligió una tipificación que tiene configurada una\nautomatización de webhook, y esa automatización apunta a este endpoint.\n\nNo se suscribe: lo manda la tipificación, no tu lista de eventos. Sirve\npara enterarte SOLO de los resultados que te importan (\"interesado\") en vez de recibir todas las llamadas con `call.wrapped_up`.\n\nEl cuerpo es el mismo de una llamada, con `disposition` adentro. Ojo con\nuna transferencia: cada agente tipifica su propia pata, así que podés\nrecibir dos, y el `id` de una pata no aparece en `GET /calls` (que\ndevuelve una fila por conversación) aunque sí se puede leer con\n`GET /calls/{id}`.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "contact_import.completed": {
      "post": {
        "summary": "Importación terminada",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer ds_...`\n\nLa clave se crea en Configuración → API y se muestra una sola vez.\nNunca la pongas en un navegador: una clave en el navegador es una clave\nfiltrada.\n"
      }
    },
    "schemas": {
      "Contact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "external_id": {
            "type": "string",
            "nullable": true
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Con fecha, el contacto está archivado, fuera de todo camino de\ndiscado. Se devuelve igual que cualquier otro: una sincronización\nque no lo ve lo vuelve a crear.\n"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "phones": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "En formato E.164. El primero es el principal."
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "company": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "Campos personalizados, por id."
          },
          "list_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "compliance": {
            "type": "object",
            "description": "Sólo lectura. Se cambia desde la app, nunca desde la API.",
            "properties": {
              "dnc_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "blocked_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "sms_opt_out_phones": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Presente sólo si hay algo que avisar. `timezone_missing` significa\nque el contacto NO se va a llamar hasta que tenga zona horaria: la\nventana legal falla cerrada ante una zona desconocida.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ContactWrite": {
        "type": "object",
        "required": [
          "name",
          "phones"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "phones": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "company": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fields": {
            "type": "object",
            "additionalProperties": true
          },
          "list_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ContactPatch": {
        "type": "object",
        "properties": {
          "external_id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "phones": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Se SUMAN a los que ya tiene."
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "company": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Se SUMAN a las que ya tiene."
          },
          "fields": {
            "type": "object",
            "additionalProperties": true
          },
          "list_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "remove_phones": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "remove_tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "remove_fields": {
            "type": "object",
            "additionalProperties": true
          },
          "remove_list_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Field": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "internal_name": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "is_system": {
            "type": "boolean"
          },
          "is_required": {
            "type": "boolean"
          },
          "is_active": {
            "type": "boolean"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "key": {
            "type": "string",
            "description": "Dónde va el valor en el cuerpo del pedido."
          }
        }
      },
      "Import": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "total_rows": {
            "type": "integer"
          },
          "processed_rows": {
            "type": "integer"
          },
          "existing_contacts": {
            "type": "string",
            "enum": [
              "update",
              "keep"
            ]
          },
          "created": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "unchanged": {
            "type": "integer",
            "description": "Filas de contactos que ya existían y la importación dejó como estaban (`existing_contacts=keep`)."
          },
          "suppressed": {
            "type": "integer",
            "description": "Filas que entraron y quedaron marcadas. No es una categoría de fallo."
          },
          "failed": {
            "type": "integer"
          },
          "failure_reason": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Call": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "ringing",
              "in_progress",
              "parked",
              "completed",
              "missed",
              "voicemail",
              "failed"
            ]
          },
          "outcome": {
            "type": "string",
            "nullable": true,
            "enum": [
              "answered",
              "no_answer",
              "busy",
              "voicemail",
              "failed",
              "abandoned",
              "no_verdict",
              "canceled",
              "provider_error"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "contact_id": {
            "type": "string",
            "nullable": true
          },
          "agent": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "campaign": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "inbox": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "disposition": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "category": {
                "type": "string"
              },
              "outcome": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Lo que escribió el agente al tipificar."
          },
          "duration_seconds": {
            "type": "integer",
            "nullable": true
          },
          "has_recording": {
            "type": "boolean",
            "description": "Si hay audio. El audio no se entrega por la API."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "answered_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "paused",
              "archived"
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "voice",
              "whatsapp",
              "sms"
            ],
            "description": "`voice` llama a sus contactos; `whatsapp` le manda a cada uno una plantilla aprobada, una sola vez; `sms` le manda a cada uno un mensaje de texto, una sola vez. Sumar contactos funciona igual en las tres."
          },
          "dial_mode": {
            "type": "string",
            "description": "Cómo disca una campaña de llamadas. Una de WhatsApp no lo usa."
          },
          "accepts_enrollment": {
            "type": "boolean",
            "description": "Si sumarle contactos ahora mismo va a funcionar."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ContactList": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "is_segment": {
            "type": "boolean",
            "description": "Una lista segmentada arma sus miembros sola; no se le agregan contactos."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Note": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "contact_id": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "call_id": {
            "type": "string",
            "nullable": true,
            "description": "Presente cuando la nota viene del cierre de una llamada."
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "agent",
                  "api_key"
                ]
              },
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Conversation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "contact_id": {
            "type": "string"
          },
          "line": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "inbox": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ]
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "unanswered": {
            "type": "boolean"
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_inbound_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "closed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConversationItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "conversation_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "message",
              "call"
            ]
          },
          "channel": {
            "type": "string",
            "nullable": true,
            "enum": [
              "sms",
              "whatsapp"
            ]
          },
          "direction": {
            "type": "string",
            "nullable": true,
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "body": {
            "type": "string",
            "nullable": true
          },
          "call_id": {
            "type": "string",
            "nullable": true
          },
          "delivery_status": {
            "type": "string",
            "nullable": true
          },
          "error_code": {
            "type": "string",
            "nullable": true
          },
          "agent": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "media": {
            "type": "array",
            "description": "Los adjuntos, descriptos. El archivo no se entrega por la API.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "mime": {
                  "type": "string"
                },
                "size": {
                  "type": "integer"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Callback": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "contact_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "cancelled",
              "expired"
            ]
          },
          "due_at": {
            "type": "string",
            "format": "date-time"
          },
          "window_end": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Presente cuando se acordó una franja y no un horario exacto."
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "agent": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "campaign": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "call_id": {
            "type": "string",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "El sobre de todo aviso. La firma viaja en `DialSprint-Signature`\n(`t=<unix>,v1=<hmac sha256 de \"t.cuerpo\">`) y el identificador de la\nentrega en `DialSprint-Delivery`.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del evento. Sirve para descartar repetidos."
          },
          "type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ContactEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/Contact"
              }
            }
          }
        ]
      },
      "CallEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/Call"
              }
            }
          }
        ]
      },
      "ImportEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/Import"
              }
            }
          }
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Para el desarrollador."
          },
          "message_code": {
            "type": "string",
            "description": "Clave estable para ramificar."
          },
          "request_id": {
            "type": "string",
            "description": "Citalo si nos escribís."
          },
          "errors": {
            "type": "object",
            "additionalProperties": true,
            "description": "Detalle por campo, en las validaciones."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Falta la clave o no sirve (`api_key_missing`, `api_key_invalid`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "La cuenta no está activa (`account_not_active`) o está bloqueada porque la prueba terminó sin carga de saldo (`tenant_locked`). También responde 402 una cuenta suspendida (`tenant_suspended`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "La clave no tiene el permiso necesario (`api_scope_missing`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No existe, o no es de esta cuenta (`not_found`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "El teléfono ya pertenece a otro contacto (`contact_exists`). Trae\n`contact_id`, y `matched_contact_id` cuando tu identificador apuntaba a\notro contacto distinto.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Invalid": {
        "description": "El cuerpo no pasó la validación (`validation_failed`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Demasiados pedidos (`rate_limited`). 120 por minuto por clave, y un\ntecho por cuenta por encima de eso. Mirá `Retry-After` y\n`X-RateLimit-Remaining`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
