{
  "openapi": "3.1.0",
  "info": {
    "title": "Nabbu API",
    "version": "1.1.0",
    "description": "API pública de Nabbu: sincroniza contactos y consentimientos, crea plantillas y campañas de correo, envía correos desde tus propios formularios y recibe eventos por webhook.\n\n## Autenticación\n\nLos administradores crean claves en **Configuración → API y webhooks**. La clave se muestra una sola vez; envíala en cada solicitud:\n\n```\nAuthorization: Bearer deye_3f9c0a1b2c4d_Zm9vYmFyYmF6...\n```\n\nTodo queda limitado a la organización dueña de la clave. **Nunca pongas la clave en código que corre en el navegador**: llama a la API desde tu servidor.\n\n## Permisos (scopes)\n\n| Permiso | Permite |\n| --- | --- |\n| `contacts:read` | Listar y consultar contactos |\n| `contacts:write` | Crear o actualizar contactos y registrar consentimientos |\n| `templates:read` | Listar y consultar plantillas de correo |\n| `templates:write` | Crear plantillas de correo |\n| `campaigns:read` | Listar campañas y descargar resultados |\n| `campaigns:write` | Crear y lanzar campañas de correo |\n| `emails:send` | Enviar el correo de una campaña activada por API a un contacto (lo crea si no existe) |\n\n`GET /me` funciona con cualquier clave válida. Cuando la organización está suspendida, la API es de solo lectura (`403 organization_inactive`).\n\n## Dos formas de enviar correos\n\n**Campaña masiva** (`trigger: \"manual\"`): crea la plantilla, crea la campaña con su audiencia (`person_ids` o `all_project_contacts`) y lánzala ahora o programada. Cada contacto recibe el correo una vez.\n\n**Envío por formulario** (`trigger: \"api\"`): para formularios de contacto, registros o cualquier flujo de \"una persona a la vez\". Crea **una** campaña por flujo (por ejemplo \"Bienvenida formulario web\"), lánzala una vez y llama a `POST /campaigns/{campaign_id}/send` cada vez que alguien deja sus datos. La llamada crea o actualiza el contacto, lo agrega al proyecto, registra su autorización de correo y pone el correo en cola; sale en menos de un minuto. La campaña reúne las métricas de todo el flujo sin llenar la lista de campañas.\n\n```\nPOST /email-templates                  → plantilla\nPOST /campaigns  { \"trigger\": \"api\" }  → campaña (borrador)\nPOST /campaigns/{id}/launch            → campaña activa\nPOST /campaigns/{id}/send              → un correo por contacto\n```\n\nLas reglas de cumplimiento son las mismas de la aplicación: enlace de desuscripción en cada correo, lista de supresión (rebotes y quejas), consentimiento por proyecto (Ley 1581) y cupo mensual de mensajes.\n\n## Errores\n\n```json\n{ \"error\": { \"code\": \"validation_error\", \"message\": \"email no es un correo válido.\", \"field\": \"email\" } }\n```\n\nLos mensajes están en español; integra contra `error.code`.\n\n## Paginación\n\nLas listas se ordenan por id y usan cursor: `?limit=100&starting_after=4521`. La respuesta trae `has_more` y `next_cursor`; pasa `next_cursor` como `starting_after` para la página siguiente.\n\n## Límites\n\n120 solicitudes por minuto por clave (`429 rate_limited` con `Retry-After`). Las claves inválidas cuentan por IP: 30 intentos fallidos en 10 minutos bloquean la IP el resto de la ventana.\n\n## Webhooks\n\nEventos firmados con `X-DEYE-Signature: t=<unix>,v1=<hex HMAC-SHA256(secreto, \"<t>.<cuerpo>\")>`. Verifica la firma sobre el cuerpo crudo y rechaza marcas de tiempo de más de 5 minutos. Los eventos están al final de esta referencia.\n"
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Este servidor"
    }
  ],
  "tags": [
    {
      "name": "Organización",
      "description": "La organización de la clave, sus proyectos y permisos."
    },
    {
      "name": "Contactos",
      "description": "Destinatarios de la organización. Se identifican por email o celular dentro de la organización."
    },
    {
      "name": "Consentimientos",
      "description": "Autorizaciones por proyecto y canal (Ley 1581). Cada cambio queda en el historial con fuente `api`."
    },
    {
      "name": "Plantillas de correo",
      "description": "Asunto y cuerpo HTML de los correos. El HTML se sanea y los campos de personalización se validan al guardar: `{{nombre}}`, `{{email}}`, `{{municipio}}`, `{{vereda}}`, `{{barrio}}`, `{{proyecto}}`, `{{enlace_desuscripcion}}` (admiten respaldo: `{{nombre|vecino}}`)."
    },
    {
      "name": "Campañas",
      "description": "Campañas de correo: masivas (`trigger: manual`) o activadas por API (`trigger: api`). Lanzar solo valida y pone en cola; el envío lo hace el servicio en segundo plano."
    },
    {
      "name": "Envíos por API",
      "description": "Un correo para un contacto a la vez, a través de una campaña activa con `trigger: api`. Pensado para formularios de contacto y registros."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Datos",
      "tags": [
        "Organización",
        "Contactos",
        "Consentimientos"
      ]
    },
    {
      "name": "Correo",
      "tags": [
        "Plantillas de correo",
        "Campañas",
        "Envíos por API"
      ]
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "summary": "Organización, proyectos y permisos de la clave",
        "responses": {
          "200": {
            "description": "Organización",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Organización"
        ],
        "operationId": "getMe",
        "description": "Cualquier clave válida."
      }
    },
    "/contacts": {
      "get": {
        "summary": "Listar contactos",
        "x-scope": "contacts:read",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "celular_e164",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Valor de next_cursor de la página anterior"
          }
        ],
        "responses": {
          "200": {
            "description": "Página de contactos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Parámetros inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Contactos"
        ],
        "operationId": "listContacts",
        "description": "Permiso: `contacts:read`."
      },
      "put": {
        "summary": "Crear o actualizar contactos (por email o celular)",
        "x-scope": "contacts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ContactInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "contacts": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "$ref": "#/components/schemas/ContactInput"
                        }
                      }
                    },
                    "required": [
                      "contacts"
                    ]
                  }
                ]
              },
              "examples": {
                "uno": {
                  "summary": "Un contacto en un proyecto",
                  "value": {
                    "nombre": "Ana Pérez",
                    "email": "ana@example.com",
                    "celular_e164": "+573001112233",
                    "project_ids": [
                      27
                    ]
                  }
                },
                "lote": {
                  "summary": "Lote",
                  "value": {
                    "contacts": [
                      {
                        "nombre": "Ana",
                        "email": "ana@example.com",
                        "project_ids": [
                          27
                        ]
                      },
                      {
                        "nombre": "Luis",
                        "celular_e164": "+573004445566",
                        "project_ids": [
                          27
                        ]
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado (o resultado de lote)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContactUpsertResult"
                    },
                    {
                      "$ref": "#/components/schemas/ContactBatchResult"
                    }
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactUpsertResult"
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso u organización inactiva",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Proyecto inexistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflicto, contacto ambiguo o suprimido (contact_erased)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Datos inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Contactos"
        ],
        "operationId": "upsertContacts",
        "description": "Busca un contacto de la organización por `email` (sin distinguir mayúsculas) o `celular_e164`. Si existe, actualiza los campos enviados (`null` borra email o celular); si no, lo crea (`nombre` obligatorio). `project_ids` lo agrega a esos proyectos sin quitarlo de otros.\n\nLote: `{ \"contacts\": [...] }` con hasta 100 elementos; responde 200 con un resultado por elemento.\n\nUna persona que pidió la supresión de sus datos queda bloqueada (`409 contact_erased`).\n\nPermiso: `contacts:write`."
      },
      "post": {
        "summary": "Igual que PUT /contacts",
        "x-scope": "contacts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ContactInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "contacts": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "$ref": "#/components/schemas/ContactInput"
                        }
                      }
                    },
                    "required": [
                      "contacts"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado (o resultado de lote)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContactUpsertResult"
                    },
                    {
                      "$ref": "#/components/schemas/ContactBatchResult"
                    }
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactUpsertResult"
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso u organización inactiva",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Proyecto inexistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflicto, contacto ambiguo o suprimido (contact_erased)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Datos inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Contactos"
        ],
        "operationId": "upsertContactsPost",
        "description": "Permiso: `contacts:write`."
      }
    },
    "/contacts/{person_id}": {
      "get": {
        "summary": "Un contacto con sus consentimientos",
        "x-scope": "contacts:read",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contacto",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ContactWithConsents"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No existe en la organización",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Contactos"
        ],
        "operationId": "getContact",
        "description": "Permiso: `contacts:read`."
      }
    },
    "/contact-fields": {
      "get": {
        "summary": "Listar los campos de contacto",
        "x-scope": "contacts:read",
        "responses": {
          "200": {
            "description": "Campos activos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ContactField"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el permiso contacts:read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/consents": {
      "put": {
        "summary": "Registrar una decisión de consentimiento",
        "x-scope": "contacts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConsentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Consentimiento guardado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "person_id": {
                          "type": "integer"
                        },
                        "consent": {
                          "$ref": "#/components/schemas/Consent"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso u organización inactiva",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Contacto o proyecto inexistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Datos inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Consentimientos"
        ],
        "operationId": "upsertConsent",
        "description": "Registra la decisión de un contacto para un proyecto y canal. Queda en el historial de consentimientos con fuente `api` y dispara el webhook `consent.changed`.\n\nPermiso: `contacts:write`."
      },
      "post": {
        "summary": "Igual que PUT /consents",
        "x-scope": "contacts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConsentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Consentimiento guardado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "person_id": {
                          "type": "integer"
                        },
                        "consent": {
                          "$ref": "#/components/schemas/Consent"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso u organización inactiva",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Contacto o proyecto inexistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Datos inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Consentimientos"
        ],
        "operationId": "upsertConsentPost",
        "description": "Permiso: `contacts:write`."
      }
    },
    "/email-templates": {
      "get": {
        "tags": [
          "Plantillas de correo"
        ],
        "operationId": "listEmailTemplates",
        "summary": "Listar plantillas de correo",
        "description": "Plantillas de los proyectos de la organización, sin los cuerpos.\n\nPermiso: `templates:read`.",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "approved",
                "archived"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Valor de next_cursor de la página anterior"
          }
        ],
        "responses": {
          "200": {
            "description": "Página de plantillas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EmailTemplate"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "tags": [
          "Plantillas de correo"
        ],
        "operationId": "createEmailTemplate",
        "summary": "Crear una plantilla de correo",
        "description": "Mismas reglas que en la aplicación: el HTML se sanea y un campo de personalización desconocido es un error (`422`). Las direcciones de enlaces e imágenes no pueden empezar con un campo (salvo `{{enlace_desuscripcion}}`).\n\nPermiso: `templates:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplateInput"
              },
              "example": {
                "project_id": 27,
                "name": "Bienvenida formulario web",
                "subject": "Gracias por escribirnos, {{nombre|vecino}}",
                "html_body": "<p>Hola {{nombre|vecino}},</p><p>Recibimos tu mensaje y te responderemos pronto.</p><p><a href=\"{{enlace_desuscripcion}}\">Dejar de recibir correos</a></p>",
                "status": "approved"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EmailTemplateWithBody"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Proyecto inexistente (`project_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Ya existe una plantilla con ese nombre en el proyecto (`template_name_taken`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Contenido inválido: asunto vacío o multilínea, HTML vacío, campo desconocido (`validation_error`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/email-templates/{email_template_id}": {
      "get": {
        "tags": [
          "Plantillas de correo"
        ],
        "operationId": "getEmailTemplate",
        "summary": "Una plantilla con su HTML y texto",
        "description": "Permiso: `templates:read`.",
        "parameters": [
          {
            "name": "email_template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plantilla",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EmailTemplateWithBody"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "La plantilla no existe en la organización (`template_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "summary": "Listar campañas",
        "x-scope": "campaigns:read",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "launching",
                "active",
                "paused",
                "ended"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Valor de next_cursor de la página anterior"
          }
        ],
        "responses": {
          "200": {
            "description": "Página de campañas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Campañas"
        ],
        "operationId": "listCampaigns",
        "description": "Permiso: `campaigns:read`."
      },
      "post": {
        "tags": [
          "Campañas"
        ],
        "operationId": "createCampaign",
        "summary": "Crear una campaña de correo",
        "description": "Crea la campaña en borrador; lánzala con `POST /campaigns/{campaign_id}/launch`.\n\n- `trigger: \"manual\"` (por defecto): indica la audiencia con `person_ids` (contactos del proyecto) o `all_project_contacts: true`.\n- `trigger: \"api\"`: sin audiencia. Después de lanzarla, cada `POST /campaigns/{campaign_id}/send` envía el correo a un contacto.\n\nPermiso: `campaigns:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignCreateInput"
              },
              "examples": {
                "masiva": {
                  "summary": "Masiva a todo el proyecto",
                  "value": {
                    "project_id": 27,
                    "name": "Boletín octubre",
                    "email_template_id": 12,
                    "all_project_contacts": true
                  }
                },
                "formulario": {
                  "summary": "Activada por API (formulario de contacto)",
                  "value": {
                    "project_id": 27,
                    "name": "Bienvenida formulario web",
                    "email_template_id": 13,
                    "trigger": "api"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creada (borrador)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Proyecto, plantilla o remitente inexistente (`project_not_found`, `template_not_found`, `sender_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Plantilla archivada (`template_archived`), contactos fuera del proyecto (`contacts_not_in_project`) o proyecto sin contactos (`no_recipients`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/campaigns/{campaign_id}": {
      "get": {
        "tags": [
          "Campañas"
        ],
        "operationId": "getCampaign",
        "summary": "Una campaña con sus totales",
        "description": "Útil para seguir el estado tras lanzarla.\n\nPermiso: `campaigns:read`.",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaña",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "La campaña no existe en la organización (`campaign_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/campaigns/{campaign_id}/launch": {
      "post": {
        "tags": [
          "Campañas"
        ],
        "operationId": "launchCampaign",
        "summary": "Lanzar o programar una campaña",
        "description": "Lanza un borrador ahora (cuerpo vacío o `mode: now`) o lo programa (`mode: scheduled` con `launch_at`). Valida plantilla, remitente verificado y cupo mensual; descarta a quien no tiene correo válido, está en la lista de supresión o no autorizó el correo, y pone el resto en cola.\n\nEn una campaña activada por API, lanzar solo la abre para `POST /campaigns/{campaign_id}/send`.\n\nPermiso: `campaigns:write`.",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LaunchInput"
              },
              "examples": {
                "ahora": {
                  "summary": "Ahora",
                  "value": {
                    "mode": "now"
                  }
                },
                "programada": {
                  "summary": "Programada",
                  "value": {
                    "mode": "scheduled",
                    "launch_at": "2026-11-03T09:00:00-05:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lanzada o programada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LaunchResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "La campaña no existe en la organización (`campaign_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La campaña no está en borrador (`campaign_not_draft`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No se pudo lanzar (`launch_failed`): sin remitente verificado, cupo mensual, nadie puede recibirla… El mensaje explica el motivo. También `unsupported_channel` para WhatsApp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/campaigns/{campaign_id}/results": {
      "get": {
        "summary": "Destinatarios y respuestas de una campaña",
        "x-scope": "campaigns:read",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Valor de next_cursor de la página anterior"
          }
        ],
        "responses": {
          "200": {
            "description": "Resultados",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CampaignResult"
                          }
                        },
                        "has_more": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "data",
                        "has_more",
                        "next_cursor"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "campaign": {
                          "$ref": "#/components/schemas/Campaign"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No existe en la organización",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clave ausente, inválida, revocada o vencida",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Campañas"
        ],
        "operationId": "getCampaignResults",
        "description": "Destinatarios de la campaña con su estado de entrega y, en encuestas, una respuesta por pregunta. En campañas activadas por API, cada envío aparece aquí.\n\nPermiso: `campaigns:read`."
      }
    },
    "/campaigns/{campaign_id}/send": {
      "post": {
        "tags": [
          "Envíos por API"
        ],
        "operationId": "sendCampaignEmail",
        "summary": "Enviar el correo a un contacto",
        "description": "Envía el correo de una campaña **activa** con `trigger: \"api\"` a un contacto:\n\n1. Crea o actualiza el contacto (`contact`, por email o celular) y lo agrega al proyecto de la campaña.\n2. Si viene `consent`, registra la autorización de correo para ese proyecto (historial Ley 1581, fuente `api`).\n3. Revisa lista de supresión, consentimiento y cupo mensual, y pone el correo en cola. Sale en menos de un minuto.\n\nCada contacto recibe el correo de una campaña **una sola vez**: repetir la llamada responde `200` con `result: \"duplicate\"` y no reenvía nada, así que es seguro reintentar.\n\nEl resultado de la entrega aparece en `GET /campaigns/{campaign_id}/results`.\n\nPermiso: `emails:send`. Esta clave solo envía; no lee contactos.",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TriggeredSendInput"
              },
              "examples": {
                "formulario": {
                  "summary": "Formulario de contacto",
                  "value": {
                    "contact": {
                      "nombre": "Ana Pérez",
                      "email": "ana@example.com",
                      "celular_e164": "+573001112233"
                    },
                    "consent": {
                      "status": "granted",
                      "legal_basis": "Formulario web: casilla de autorización de datos"
                    }
                  }
                },
                "existente": {
                  "summary": "Contacto existente",
                  "value": {
                    "person_id": 460
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "En cola",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriggeredSendResult"
                },
                "example": {
                  "data": {
                    "result": "queued",
                    "campaign_id": 91,
                    "person_id": 460,
                    "contact_result": "created",
                    "send_state": "queued",
                    "delivery_status": "queued"
                  }
                }
              }
            }
          },
          "200": {
            "description": "Ya enviado a este contacto: no se reenvía",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriggeredSendResult"
                },
                "example": {
                  "data": {
                    "result": "duplicate",
                    "campaign_id": 91,
                    "person_id": 460,
                    "contact_result": "updated",
                    "send_state": "sent",
                    "delivery_status": "sent"
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sin permiso, organización inactiva o cupo mensual agotado (`monthly_quota_exceeded`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Campaña o contacto inexistente (`campaign_not_found`, `contact_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`campaign_not_triggerable` (no es trigger api), `campaign_not_active`, `email_suppressed`, `consent_revoked`, `consent_required`, `contact_erased`, `contact_conflict`, `contact_ambiguous`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cuerpo inválido, contacto sin correo o dominio que no recibe correo (`validation_error`, `contact_without_email`, `email_undeliverable`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "response.received": {
      "post": {
        "summary": "Un destinatario respondió una pregunta",
        "parameters": [
          {
            "name": "X-DEYE-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Event",
            "in": "header",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Delivery",
            "in": "header",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "data": {
                        "$ref": "#/components/schemas/ResponseReceivedData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido"
          }
        }
      }
    },
    "consent.changed": {
      "post": {
        "summary": "Cambió un consentimiento",
        "parameters": [
          {
            "name": "X-DEYE-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Event",
            "in": "header",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Delivery",
            "in": "header",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "data": {
                        "$ref": "#/components/schemas/ConsentChangedData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido"
          }
        }
      }
    },
    "campaign.finished": {
      "post": {
        "summary": "Terminó una campaña",
        "parameters": [
          {
            "name": "X-DEYE-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Event",
            "in": "header",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-DEYE-Delivery",
            "in": "header",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "data": {
                        "$ref": "#/components/schemas/CampaignFinishedData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "deye_<12 hex>_<43 base64url>"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "field": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "organization": {
            "type": "object",
            "properties": {
              "organization_id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "projects": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "project_id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "api_key": {
            "type": "object",
            "properties": {
              "prefix": {
                "type": "string"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "contacts:read",
                    "contacts:write",
                    "campaigns:read"
                  ]
                }
              }
            }
          }
        }
      },
      "ContactInput": {
        "type": "object",
        "properties": {
          "nombre": {
            "type": "string",
            "maxLength": 200
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email"
          },
          "celular_e164": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+[1-9]\\d{7,14}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "blocked"
            ]
          },
          "project_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "maxItems": 50
          },
          "custom_fields": {
            "type": "object",
            "description": "Valores por clave de campo (GET /contact-fields). Texto, número, fecha AAAA-MM-DD, true/false; las opciones por nombre o id (selección múltiple: lista). null borra el valor. Solo se cambian las claves enviadas.",
            "additionalProperties": true,
            "maxProperties": 100
          }
        }
      },
      "Contact": {
        "type": "object",
        "properties": {
          "person_id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "celular_e164": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "project_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "custom_fields": {
            "type": "object",
            "description": "Valores de los campos de contacto de la organización, por clave. Las opciones se devuelven con su nombre (selección múltiple: lista de nombres).",
            "additionalProperties": true
          }
        }
      },
      "ContactWithConsents": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Contact"
          },
          {
            "type": "object",
            "properties": {
              "consents": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Consent"
                }
              }
            }
          }
        ]
      },
      "ContactUpsertResult": {
        "type": "object",
        "properties": {
          "result": {
            "type": "string",
            "enum": [
              "created",
              "updated"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/ContactWithConsents"
          }
        }
      },
      "ContactBatchResult": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "result": {
                  "type": "string",
                  "enum": [
                    "created",
                    "updated",
                    "error"
                  ]
                },
                "data": {
                  "$ref": "#/components/schemas/ContactWithConsents"
                },
                "error": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "field": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ConsentInput": {
        "type": "object",
        "required": [
          "project_id",
          "channel",
          "status"
        ],
        "properties": {
          "person_id": {
            "type": "integer"
          },
          "email": {
            "type": "string"
          },
          "celular_e164": {
            "type": "string"
          },
          "project_id": {
            "type": "integer"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "email"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "granted",
              "revoked"
            ]
          },
          "legal_basis": {
            "type": "string",
            "maxLength": 200
          },
          "notes": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "Consent": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "integer"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "email"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "unknown",
              "granted",
              "revoked"
            ]
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "legal_basis": {
            "type": [
              "string",
              "null"
            ]
          },
          "granted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "integer"
          },
          "project_id": {
            "type": "integer"
          },
          "project_name": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "email"
            ]
          },
          "campaign_type": {
            "type": "string",
            "enum": [
              "survey",
              "message"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "launching",
              "active",
              "paused",
              "ended"
            ]
          },
          "trigger": {
            "type": "string",
            "enum": [
              "manual",
              "api"
            ],
            "description": "manual: audiencia fija al crearla. api: los contactos se agregan uno a uno con POST /campaigns/{campaign_id}/send."
          },
          "email_template_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "duration_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Días tras el lanzamiento hasta que finaliza sola"
          },
          "launch_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Motivo del último lanzamiento fallido"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "totals": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              },
              "sent": {
                "type": "integer"
              },
              "responded": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              }
            }
          }
        }
      },
      "CampaignResult": {
        "type": "object",
        "properties": {
          "person_id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "celular_e164": {
            "type": [
              "string",
              "null"
            ]
          },
          "delivery_status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "failed",
              "responded"
            ]
          },
          "answers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "question_id": {
                  "type": "integer"
                },
                "position": {
                  "type": "integer"
                },
                "prompt": {
                  "type": "string"
                },
                "question_type": {
                  "type": "string"
                },
                "answered_at": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "answer": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "organization_id": {
            "type": "integer"
          },
          "data": {
            "type": "object"
          }
        }
      },
      "WebhookContact": {
        "type": "object",
        "properties": {
          "person_id": {
            "type": "integer"
          },
          "nombre": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "celular_e164": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "description": "Valores de los campos de contacto de la organización, por clave. Las opciones se devuelven con su nombre (selección múltiple: lista de nombres).",
            "additionalProperties": true
          }
        }
      },
      "ResponseReceivedData": {
        "type": "object",
        "properties": {
          "response_id": {
            "type": "integer"
          },
          "answered_at": {
            "type": "string"
          },
          "campaign": {
            "type": "object"
          },
          "question": {
            "type": "object"
          },
          "answer": {
            "type": "object",
            "properties": {
              "display": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "number": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "boolean": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "date": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "option_ids": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          },
          "contact": {
            "$ref": "#/components/schemas/WebhookContact"
          }
        }
      },
      "ConsentChangedData": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "integer"
          },
          "project_name": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "previous_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "legal_basis": {
            "type": [
              "string",
              "null"
            ]
          },
          "changed_at": {
            "type": "string"
          },
          "contact": {
            "$ref": "#/components/schemas/WebhookContact"
          }
        }
      },
      "CampaignFinishedData": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "project_id": {
            "type": "integer"
          },
          "project_name": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "campaign_type": {
            "type": "string"
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "ends_at": {
            "type": "string"
          },
          "totals": {
            "type": "object"
          }
        }
      },
      "EmailTemplate": {
        "type": "object",
        "properties": {
          "email_template_id": {
            "type": "integer"
          },
          "project_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "approved",
              "archived"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "EmailTemplateWithBody": {
        "type": "object",
        "properties": {
          "email_template_id": {
            "type": "integer"
          },
          "project_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "approved",
              "archived"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "html_body": {
            "type": "string",
            "description": "HTML saneado"
          },
          "text_body": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "EmailTemplateInput": {
        "type": "object",
        "required": [
          "project_id",
          "name",
          "subject",
          "html_body"
        ],
        "properties": {
          "project_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Único por proyecto"
          },
          "subject": {
            "type": "string",
            "maxLength": 200,
            "description": "Una línea; admite campos de personalización"
          },
          "html_body": {
            "type": "string",
            "maxLength": 400000,
            "description": "Se sanea: solo etiquetas seguras para correo, sin scripts ni eventos"
          },
          "text_body": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100000,
            "description": "Versión de texto; si falta se genera desde el HTML"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "approved"
            ],
            "default": "draft"
          }
        }
      },
      "CampaignCreateInput": {
        "type": "object",
        "required": [
          "project_id",
          "name",
          "email_template_id"
        ],
        "properties": {
          "project_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "channel": {
            "type": "string",
            "enum": [
              "email"
            ],
            "default": "email",
            "description": "Por ahora solo correo"
          },
          "email_template_id": {
            "type": "integer",
            "description": "Plantilla del mismo proyecto, no archivada"
          },
          "email_sender_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Remitente verificado del proyecto; por defecto el remitente predeterminado"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "manual",
              "api"
            ],
            "default": "manual"
          },
          "person_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "minItems": 1,
            "maxItems": 10000,
            "description": "Solo manual: contactos del proyecto"
          },
          "all_project_contacts": {
            "type": "boolean",
            "description": "Solo manual: todos los contactos del proyecto"
          },
          "require_email_consent": {
            "type": "boolean",
            "default": true,
            "description": "Enviar solo a quien autorizó el correo en el proyecto"
          },
          "duration_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "LaunchInput": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "now",
              "scheduled"
            ],
            "default": "now"
          },
          "launch_at": {
            "type": "string",
            "format": "date-time",
            "description": "Obligatorio con scheduled; ISO 8601 con zona horaria"
          }
        }
      },
      "LaunchResult": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Campaign"
          },
          "launch": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "now",
                  "scheduled"
                ]
              },
              "queued": {
                "type": "integer",
                "description": "Correos en cola (0 en una campaña activada por API)"
              },
              "skipped": {
                "type": "integer"
              },
              "skipped_reasons": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                },
                "description": "sin_correo_valido, correo_duplicado, en_lista_de_supresion, dominio_sin_correo, consentimiento_revocado, sin_consentimiento"
              },
              "total": {
                "type": "integer"
              }
            }
          }
        }
      },
      "TriggeredSendInput": {
        "type": "object",
        "description": "Envía `contact` (se crea o actualiza por email/celular) o `person_id` de un contacto existente.",
        "properties": {
          "contact": {
            "type": "object",
            "required": [
              "email"
            ],
            "properties": {
              "nombre": {
                "type": "string",
                "maxLength": 200,
                "description": "Obligatorio si el contacto es nuevo"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "celular_e164": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^\\+[1-9]\\d{7,14}$"
              }
            }
          },
          "person_id": {
            "type": "integer"
          },
          "consent": {
            "type": "object",
            "description": "Autorización de correo para el proyecto de la campaña, registrada antes del envío",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "granted"
                ]
              },
              "legal_basis": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 200,
                "description": "Evidencia, ej. \"Formulario web, casilla de autorización\""
              },
              "notes": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 500
              }
            }
          }
        }
      },
      "TriggeredSendResult": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "result": {
                "type": "string",
                "enum": [
                  "queued",
                  "duplicate"
                ],
                "description": "duplicate: el contacto ya recibió (o tiene en cola) el correo de esta campaña; no se reenvía"
              },
              "campaign_id": {
                "type": "integer"
              },
              "person_id": {
                "type": "integer"
              },
              "contact_result": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "created",
                  "updated",
                  null
                ]
              },
              "send_state": {
                "type": "string",
                "enum": [
                  "queued",
                  "sending",
                  "sent",
                  "failed",
                  "skipped"
                ]
              },
              "delivery_status": {
                "type": "string",
                "enum": [
                  "queued",
                  "sent",
                  "failed",
                  "responded"
                ]
              }
            }
          }
        }
      },
      "ContactField": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "date",
              "boolean",
              "single_choice",
              "multi_choice"
            ]
          },
          "parent_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Campo del que depende (sus opciones cuelgan de una opción del padre)."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "label": {
                  "type": "string"
                },
                "parent_option_id": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Clave ausente, inválida, revocada o vencida",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "La clave no tiene el permiso (`insufficient_scope`) o la organización no está activa (`organization_inactive`)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Límite de solicitudes (`rate_limited`), con encabezado Retry-After",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "Error interno (`internal_error`)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Cuerpo o parámetros inválidos (`validation_error`); `field` indica el campo",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
