{
  "openapi": "3.1.0",
  "info": {
    "title": "Terrasonum VoiceGuard API",
    "version": "1.0.0",
    "description": "API de organizaciones de VoiceGuard. Permite a una organización listar a sus voceros, verificar un audio contra el certificado de uno de ellos e invitar a nuevos voceros. Desde la E9 (parte 2), también trabajar con las licencias de voz de las que es licenciataria: proponerlas, consultar si cubren un uso (respuesta firmada) y pedir una credencial firmada por cada audio generado, para su manifiesto C2PA.\n\nLo que NO hace: no registra voces (cada vocero registra la suya con su propia cuenta) y no detecta clones de IA. VoiceGuard certifica identidad y fecha: el veredicto describe cuánto se parece un audio a la voz registrada, no si es auténtico.\n\nGuía: https://terrasonum.io/api/"
  },
  "servers": [
    {
      "url": "https://api.terrasonum.io/api/v1"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/speakers": {
      "get": {
        "operationId": "listSpeakers",
        "summary": "Voceros vigentes de la organización y el estado de su certificado",
        "responses": {
          "200": {
            "description": "Lista de voceros",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Speaker"
                      }
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/verify": {
      "post": {
        "operationId": "verify",
        "summary": "Verificar un archivo de audio contra el certificado de un vocero",
        "description": "Solo contra certificados de voceros vigentes de la organización. Un certificado ajeno responde 404. Gasta cuota de la organización, compartida con /verify-url y con todas sus claves live: por defecto 1,000 al mes y 10 por hora. Solo gastan las que llegan a analizar el audio (200 y 422). Con clave test: 100 por hora por clave, sin cuota.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "certificate_id",
                  "file"
                ],
                "properties": {
                  "certificate_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "certificate_id del certificado del vocero (GET /speakers)."
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "WAV, MP3, FLAC, MP4/M4A, AAC, WebM u OGG. Hasta 50 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la verificación",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/X-Quota-Used"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "410": {
            "$ref": "#/components/responses/E410"
          },
          "413": {
            "$ref": "#/components/responses/E413"
          },
          "415": {
            "$ref": "#/components/responses/E415"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          },
          "502": {
            "$ref": "#/components/responses/E502"
          },
          "503": {
            "$ref": "#/components/responses/E503"
          }
        }
      }
    },
    "/verify-url": {
      "post": {
        "operationId": "verifyUrl",
        "summary": "Verificar el audio de una URL pública contra el certificado de un vocero",
        "description": "Mismas reglas y mismo límite que /verify.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "certificate_id",
                  "url"
                ],
                "properties": {
                  "certificate_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "certificate_id del certificado del vocero (GET /speakers)."
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la verificación",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/X-Quota-Used"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "408": {
            "$ref": "#/components/responses/E408"
          },
          "410": {
            "$ref": "#/components/responses/E410"
          },
          "413": {
            "$ref": "#/components/responses/E413"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          },
          "502": {
            "$ref": "#/components/responses/E502"
          },
          "503": {
            "$ref": "#/components/responses/E503"
          }
        }
      }
    },
    "/speaker-invitations": {
      "post": {
        "operationId": "createSpeakerInvitation",
        "summary": "Invitar a una persona a vincularse como vocero",
        "description": "Devuelve el enlace de invitación una sola vez. La persona lo abre, lee qué verá la organización, acepta con su propia cuenta (con correo verificado por Google o Microsoft) y registra su voz ella misma. Con una clave test no se crea nada.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invitación creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Invitation"
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "usage",
        "summary": "Uso de esta clave por día y operación (últimos 90 días) y cuota de la organización",
        "description": "Cuenta solo las llamadas que terminaron bien (2xx). No registra contra qué certificado se verificó. La cuota (quota) es de la organización y la comparten sus claves live; con una clave de prueba es null.",
        "responses": {
          "200": {
            "description": "Uso",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string",
                            "format": "date"
                          },
                          "operation": {
                            "type": "string",
                            "enum": [
                              "verify",
                              "verify_url",
                              "read",
                              "invite",
                              "license_read",
                              "license_propose",
                              "license_check",
                              "credential"
                            ]
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "test": {
                      "type": "boolean"
                    },
                    "quota": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Quota"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "consent_quota": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Quota"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Cuota de consultas y credenciales de licencias de voz. null con una clave de prueba."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/voice-licenses": {
      "get": {
        "operationId": "listVoiceLicenses",
        "summary": "Licencias de voz de las que la organización es licenciataria",
        "description": "Con términos privados y el uso reportado (credenciales). Con una clave de prueba, las licencias de ejemplo de la guía.",
        "responses": {
          "200": {
            "description": "Licencias",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/VoiceLicense"
                      }
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/voice-licenses/proposals": {
      "post": {
        "operationId": "proposeVoiceLicense",
        "summary": "Proponer una licencia a un titular",
        "description": "El titular la ve en su panel y la firma (o rechaza) con su 2FA. Lo identificas por el id de su certificado de voz vigente, que él te da. Caduca a los 30 días; como mucho 5 pendientes por titular. Con una clave de prueba se validan los términos y no se crea nada.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "voice_certificate_id",
                  "terms"
                ],
                "properties": {
                  "voice_certificate_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "terms": {
                    "$ref": "#/components/schemas/VoiceLicenseTerms"
                  },
                  "private_terms": {
                    "type": "object",
                    "description": "Importe, moneda, condiciones. Como mucho 2000 bytes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Propuesta creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/VoiceLicenseProposal"
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          },
          "503": {
            "$ref": "#/components/responses/E503"
          }
        }
      }
    },
    "/voice-licenses/{id}": {
      "get": {
        "operationId": "getVoiceLicense",
        "summary": "Una licencia de la que la organización es licenciataria",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Licencia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/VoiceLicense"
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/voice-licenses/{id}/check": {
      "post": {
        "operationId": "checkVoiceLicense",
        "summary": "Consultar si la licencia cubre un uso (respuesta firmada)",
        "description": "Antes de generar. Cachea la respuesta hasta valid_until (24 h como mucho); el webhook voice_license.revoked llega antes. Gasta una unidad de la cuota de consentimiento. Solo la licenciataria puede consultar: una licencia ajena da 404, igual que una que no existe. Con una clave de prueba, signature es null.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LicenseUseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta firmada (también cuando no autoriza)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "check": {
                      "$ref": "#/components/schemas/ConsentCheck"
                    },
                    "signature": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "JWS compacto ES256 sobre check."
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          },
          "503": {
            "$ref": "#/components/responses/E503"
          }
        }
      }
    },
    "/voice-licenses/{id}/credentials": {
      "post": {
        "operationId": "requestSyntheticUseCredential",
        "summary": "Pedir la credencial de un audio generado",
        "description": "Una por audio. Es también el reporte de uso. 201: emitida, con la aserción C2PA lista. 200: ya la habías pedido para este output_sha256 (no cuenta otro uso). 202: la licencia pide aprobar cada uso; llega por el webhook synthetic_use.approved o synthetic_use.rejected. 403 not_authorized con reason si la licencia no cubre el uso. duration_sec es obligatorio si la licencia se compensa por minuto generado. Gasta una unidad de la cuota de consentimiento.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LicenseUseRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "output_sha256"
                    ],
                    "properties": {
                      "output_sha256": {
                        "type": "string",
                        "pattern": "^[0-9a-f]{64}$",
                        "description": "SHA-256 del audio tal como salió del modelo."
                      },
                      "duration_sec": {
                        "type": "number",
                        "exclusiveMinimum": 0,
                        "maximum": 21600
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Credencial emitida",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IssuedCredential"
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Ya emitida antes para este audio",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IssuedCredential"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Pendiente de la aprobación del titular",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CredentialRequest"
                    },
                    "test": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          },
          "410": {
            "$ref": "#/components/responses/E410"
          },
          "503": {
            "$ref": "#/components/responses/E503"
          }
        }
      }
    }
  },
  "webhooks": {
    "speaker.joined": {
      "post": {
        "summary": "Un vocero acepta la invitación",
        "description": "data.speaker con el vocero recién vinculado.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "speaker.certificate_issued": {
      "post": {
        "summary": "Un vocero registra su voz",
        "description": "También al volver a registrarla. data.speaker.certificate dice si está sellado, con sello de tiempo y firmado.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "speaker.certificate_revoked": {
      "post": {
        "summary": "El certificado de un vocero deja de valer",
        "description": "data.revoked_certificate con el certificado revocado y el motivo. Desde ese momento /verify contra él responde 410.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "speaker.left": {
      "post": {
        "summary": "Un vocero deja de serlo",
        "description": "data.left_by: speaker (se retiró) u organization (lo desvinculaste). Desde ese momento /verify contra él responde 404.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "voice_license.signed": {
      "post": {
        "summary": "Un titular firma una licencia de voz a tu organización",
        "description": "data.license: la licencia tal como se firmó (sin términos privados), con su firma JWS y verify_url. Ver docs/e9-registro-consentimiento.md.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "voice_license.revoked": {
      "post": {
        "summary": "Una licencia de voz de tu organización se revoca",
        "description": "data.license con revoked_reason (holder: la revocó el titular; voice_deleted: borró su huella) y revoked_effective_at, cuándo deja de cubrir: al instante o al acabar el preaviso pactado.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "voice_license.expired": {
      "post": {
        "summary": "Una licencia de voz de tu organización venció",
        "description": "data.license con status expired. Solo las que vencen sin revocarse; las revocadas ya avisaron con voice_license.revoked. Puede llegar hasta unos minutos después de ends_at.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "synthetic_use.approved": {
      "post": {
        "summary": "El titular aprobó un uso que esperaba su aprobación",
        "description": "data es la credencial emitida, igual que el 201 de POST /voice-licenses/{id}/credentials: credential, signature, verify_url y c2pa_assertion.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    },
    "synthetic_use.rejected": {
      "post": {
        "summary": "El titular rechazó un uso que esperaba su aprobación",
        "description": "data.credential con lo que declaraste (use, territory, language, output_sha256) y status rejected. No se emite credencial.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Recibido. Cualquier otro código, un timeout de 10 s o una redirección cuentan como fallo y se reintenta."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer tsk_live_… o tsk_test_…. Las claves las crean los administradores de la organización en su panel de terrasonum.io; se muestran una sola vez."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_key",
                  "revoked_key",
                  "organization_suspended",
                  "invalid_request",
                  "not_found",
                  "certificate_revoked",
                  "timeout",
                  "payload_too_large",
                  "unsupported_media_type",
                  "invalid_audio",
                  "rate_limited",
                  "capacity_exceeded",
                  "quota_exceeded",
                  "upstream_error",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "certificate_id": {
                "type": "string",
                "format": "uuid",
                "description": "Solo en certificate_revoked."
              },
              "reason": {
                "type": "string",
                "description": "En certificate_revoked: holder | replaced | platform. En invalid_audio: motivo del pipeline."
              },
              "issued_at": {
                "type": "string",
                "format": "date-time"
              },
              "revoked_at": {
                "type": "string",
                "format": "date-time"
              },
              "quota": {
                "type": "integer",
                "description": "Solo en quota_exceeded: verificaciones al mes de la organización."
              },
              "used": {
                "type": "integer",
                "description": "Solo en quota_exceeded: gastadas este mes."
              },
              "hourly_limit": {
                "type": "integer",
                "description": "Solo en rate_limited de una clave live: máximo por hora de la organización."
              },
              "resets_at": {
                "type": "string",
                "format": "date-time",
                "description": "En quota_exceeded: inicio del mes siguiente (UTC). En rate_limited de una clave live: inicio de la hora siguiente."
              },
              "retry_after_sec": {
                "type": "integer",
                "description": "Solo en capacity_exceeded: segundos que conviene esperar. Es también la cabecera Retry-After."
              }
            }
          }
        }
      },
      "Speaker": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "consent_version": {
            "type": "string"
          },
          "consented_at": {
            "type": "string",
            "format": "date-time"
          },
          "certificate": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "registered_at": {
                "type": "string",
                "format": "date-time"
              },
              "sealed": {
                "type": "boolean"
              },
              "timestamped": {
                "type": "boolean"
              },
              "signed": {
                "type": "boolean"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "certificate_revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Si el vocero borró su huella de voz y no tiene otra."
          }
        }
      },
      "Verification": {
        "type": "object",
        "properties": {
          "certificate_id": {
            "type": "string",
            "format": "uuid"
          },
          "similarity_score": {
            "type": "number",
            "description": "Coseno entre la voz registrada y la del audio."
          },
          "spectral_anomaly_score": {
            "type": [
              "number",
              "null"
            ]
          },
          "authenticity_score": {
            "type": "integer"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "coincide",
              "coincidencia_parcial",
              "no_coincide",
              "no_concluyente"
            ],
            "description": "Describe el parecido con la voz registrada. No afirma que el audio sea auténtico ni que no sea un clon."
          },
          "audio_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "SHA-256 del audio verificado."
          },
          "same_recording": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "true si es el mismo archivo con el que se registró la voz. Prueba reenvío, no autenticidad."
          },
          "sealed": {
            "type": "boolean"
          },
          "model_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "thresholds_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "verdict_rules_version": {
            "type": "string"
          },
          "verification_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified_at": {
            "type": "string",
            "format": "date-time"
          },
          "test": {
            "type": "boolean",
            "description": "Presente y true solo con claves tsk_test_."
          }
        }
      },
      "Invitation": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "type": "string",
            "const": "speaker"
          },
          "email": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "link": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Quota": {
        "type": "object",
        "description": "Cuota de verificaciones live de la organización. La comparten todas sus claves live; las de prueba no la gastan. Cuenta las verificaciones que llegan a analizar el audio (200 y 422). La cuota de consentimiento (consent_quota en /usage) tiene la misma forma y cuenta consultas y credenciales de licencias de voz que terminan en 2xx.",
        "required": [
          "quota",
          "used",
          "remaining",
          "resets_at",
          "hourly_limit",
          "hourly_used",
          "hourly_remaining",
          "hour_resets_at"
        ],
        "properties": {
          "quota": {
            "type": "integer",
            "description": "Verificaciones al mes."
          },
          "used": {
            "type": "integer",
            "description": "Gastadas este mes (UTC)."
          },
          "remaining": {
            "type": "integer"
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "Inicio del mes siguiente, UTC."
          },
          "hourly_limit": {
            "type": "integer",
            "description": "Máximo por hora."
          },
          "hourly_used": {
            "type": "integer"
          },
          "hourly_remaining": {
            "type": "integer"
          },
          "hour_resets_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "Cuerpo de cada aviso. Firmado con Standard Webhooks: cabeceras webhook-id, webhook-timestamp y webhook-signature (v1,<base64 del HMAC-SHA256 de «id.timestamp.cuerpo»). Ver https://terrasonum.io/api/#webhooks.",
        "required": [
          "id",
          "type",
          "created_at",
          "organization_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id del evento; igual que la cabecera webhook-id y el mismo en cada reintento."
          },
          "type": {
            "type": "string",
            "enum": [
              "speaker.joined",
              "speaker.certificate_issued",
              "speaker.certificate_revoked",
              "speaker.left",
              "voice_license.signed",
              "voice_license.revoked",
              "voice_license.expired",
              "synthetic_use.approved",
              "synthetic_use.rejected"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "organization_id": {
            "type": "string",
            "format": "uuid"
          },
          "test": {
            "type": "boolean",
            "description": "Solo en los eventos de «Enviar prueba», con datos de ejemplo."
          },
          "data": {
            "type": "object",
            "properties": {
              "speaker": {
                "$ref": "#/components/schemas/Speaker"
              },
              "left_by": {
                "type": "string",
                "enum": [
                  "speaker",
                  "organization"
                ],
                "description": "Solo en speaker.left."
              },
              "revoked_certificate": {
                "type": "object",
                "description": "Solo en speaker.certificate_revoked.",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "revoked_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "holder",
                      "replaced"
                    ],
                    "description": "holder: el vocero borró su huella. replaced: la reemplazó por otra (llega también speaker.certificate_issued)."
                  }
                }
              },
              "license": {
                "type": "object",
                "description": "Solo en voice_license.*. La licencia sin términos privados: mismos campos que se firman (type, license_id, terms_version, voice_certificate_id, holder_name, licensee, authority, uses, territories, languages, starts_at, ends_at, compensation_mode, compensation_unit, max_uses, approval, revocation, private_terms_hash, signed_at), más status, signature, timestamp_at, revocation_requested_at, revoked_effective_at, revoked_reason y verify_url."
              },
              "credential": {
                "type": "object",
                "description": "Solo en synthetic_use.*: la credencial firmada (approved, junto a signature, verify_url y c2pa_assertion) o lo que se pidió (rejected)."
              }
            },
            "description": "speaker en los eventos speaker.*; license en los voice_license.*; credential en los synthetic_use.*."
          }
        }
      },
      "VoiceLicenseTerms": {
        "type": "object",
        "description": "Términos públicos del vocabulario vcl-1. Un término desconocido se rechaza: «el que no entiende, no autoriza». Ver https://terrasonum.io/api/#licencias.",
        "required": [
          "uses",
          "territories",
          "languages",
          "starts_at",
          "ends_at",
          "compensation_mode"
        ],
        "additionalProperties": false,
        "properties": {
          "uses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "dubbing",
                "audiobook_narration",
                "advertising",
                "conversational_agent",
                "video_game",
                "social_media",
                "model_training",
                "political",
                "sexual"
              ]
            },
            "description": "political y sexual son sensibles: el titular los confirma aparte."
          },
          "territories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Códigos ISO 3166-1 alfa-2, o [\"*\"]."
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Etiquetas BCP 47, o [\"*\"]. «es» cubre «es-MX»."
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "compensation_mode": {
            "type": "string",
            "enum": [
              "flat_period",
              "per_use",
              "none"
            ]
          },
          "compensation_unit": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "generated_minute",
              "generation",
              null
            ],
            "description": "Solo con per_use."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Tope de credenciales."
          },
          "approval": {
            "type": "string",
            "enum": [
              "none",
              "each_use"
            ],
            "description": "each_use: cada credencial espera la aprobación del titular."
          },
          "revocation": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "immediate",
                  "notice"
                ]
              },
              "notice_days": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 1,
                "maximum": 365
              }
            }
          }
        }
      },
      "VoiceLicense": {
        "type": "object",
        "description": "Una licencia firmada de la que la organización es licenciataria. Los campos que se firman (type … signed_at, en ese orden) están en el payload de signature (JWS ES256, clave publicada en https://terrasonum.io/voiceguard-clave-publica.pem). private_terms solo lo ven las partes.",
        "properties": {
          "type": {
            "type": "string",
            "const": "voiceguard.voice_license"
          },
          "license_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "not_started",
              "active",
              "expired",
              "revoked"
            ]
          },
          "terms_version": {
            "type": "string",
            "const": "vcl-1"
          },
          "voice_certificate_id": {
            "type": "string",
            "format": "uuid"
          },
          "holder_name": {
            "type": "string"
          },
          "licensee": {
            "type": "object",
            "properties": {
              "organization_id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "authority": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "holder",
                  "mandatary",
                  "designated_successor",
                  "heir"
                ]
              },
              "mandate_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "succession_id": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "uses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "territories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "compensation_mode": {
            "type": "string"
          },
          "compensation_unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ]
          },
          "approval": {
            "type": "string"
          },
          "revocation": {
            "type": "object"
          },
          "private_terms_hash": {
            "type": "string"
          },
          "signed_at": {
            "type": "string",
            "format": "date-time"
          },
          "timestamp_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Sello de la TSA-SINPE; null si no contestó ese día."
          },
          "signature": {
            "type": [
              "string",
              "null"
            ]
          },
          "revocation_requested_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_effective_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "holder",
              "voice_deleted",
              null
            ]
          },
          "verify_url": {
            "type": "string",
            "format": "uri"
          },
          "private_terms": {
            "type": [
              "object",
              "null"
            ],
            "description": "Importe, moneda, condiciones. No se publica; entra en la firma solo por su hash."
          },
          "usage": {
            "type": "object",
            "description": "El reporte de uso: credenciales emitidas, pendientes de aprobación y segundos generados.",
            "properties": {
              "credentials": {
                "type": "integer"
              },
              "pending_approvals": {
                "type": "integer"
              },
              "duration_sec": {
                "type": "number"
              }
            }
          }
        }
      },
      "LicenseUseRequest": {
        "type": "object",
        "required": [
          "use",
          "territory",
          "language"
        ],
        "properties": {
          "use": {
            "type": "string",
            "description": "Una categoría de vcl-1. Una desconocida no es un 400: se contesta not_authorized / unknown_term."
          },
          "territory": {
            "type": "string",
            "description": "ISO 3166-1 alfa-2, donde se publicará."
          },
          "language": {
            "type": "string",
            "description": "BCP 47 del audio, p. ej. es-MX."
          }
        }
      },
      "ConsentCheck": {
        "type": "object",
        "description": "Lo que se firma, en este orden. Guárdalo con signature como prueba de que consultaste.",
        "required": [
          "type",
          "license_id",
          "use",
          "territory",
          "language",
          "result",
          "reason",
          "authority",
          "valid_until",
          "checked_at"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "voiceguard.consent_check"
          },
          "license_id": {
            "type": "string",
            "format": "uuid"
          },
          "use": {
            "type": "string"
          },
          "territory": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "authorized",
              "not_authorized",
              "approval_required"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "unknown_term",
              "use_not_licensed",
              "territory",
              "language",
              "not_started",
              "expired",
              "revoked",
              "max_uses_reached",
              "each_use",
              null
            ],
            "description": "null si authorized; each_use si approval_required."
          },
          "authority": {
            "type": "string",
            "enum": [
              "holder",
              "mandatary",
              "designated_successor",
              "heir"
            ],
            "description": "Quién firmó la licencia. Nunca se dice solo «autorizado»."
          },
          "valid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Hasta cuándo puedes cachear esta respuesta: el primero entre el fin, una revocación con preaviso y 24 h. null si no autoriza."
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SyntheticUseCredential": {
        "type": "object",
        "description": "Lo que se firma, en este orden. Sin sello de la TSA: la fecha la afirma la firma de Terrasonum.",
        "properties": {
          "type": {
            "type": "string",
            "const": "voiceguard.synthetic_use"
          },
          "credential_id": {
            "type": "string",
            "format": "uuid"
          },
          "license_id": {
            "type": "string",
            "format": "uuid"
          },
          "voice_certificate_id": {
            "type": "string",
            "format": "uuid"
          },
          "holder_name": {
            "type": "string"
          },
          "licensee": {
            "type": "object",
            "properties": {
              "organization_id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "authority": {
            "type": "string",
            "enum": [
              "holder",
              "mandatary",
              "designated_successor",
              "heir"
            ]
          },
          "use": {
            "type": "string"
          },
          "territory": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "output_sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "duration_sec": {
            "type": [
              "number",
              "null"
            ]
          },
          "issued_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IssuedCredential": {
        "type": "object",
        "required": [
          "credential",
          "signature",
          "status",
          "verify_url",
          "c2pa_assertion"
        ],
        "properties": {
          "credential": {
            "$ref": "#/components/schemas/SyntheticUseCredential"
          },
          "signature": {
            "type": [
              "string",
              "null"
            ],
            "description": "JWS compacto ES256. null solo con una clave de prueba."
          },
          "signature_alg": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "const": "issued"
          },
          "verify_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "c2pa_assertion": {
            "type": [
              "object",
              "null"
            ],
            "description": "Lista para meterla en el manifiesto C2PA del audio: label io.terrasonum.voice-consent, data { version: 1, credential: <JWS>, verify_url }."
          }
        }
      },
      "CredentialRequest": {
        "type": "object",
        "description": "Una credencial que no está emitida: lo que declaraste y en qué quedó.",
        "properties": {
          "credential_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "license_id": {
            "type": "string",
            "format": "uuid"
          },
          "use": {
            "type": "string"
          },
          "territory": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "output_sha256": {
            "type": "string"
          },
          "duration_sec": {
            "type": [
              "number",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_approval",
              "approval_expired",
              "rejected"
            ]
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "approval_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "7 días para que el titular decida."
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "VoiceLicenseProposal": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "voice_certificate_id": {
            "type": "string",
            "format": "uuid"
          },
          "terms_version": {
            "type": "string"
          },
          "terms": {
            "$ref": "#/components/schemas/VoiceLicenseTerms"
          },
          "private_terms": {
            "type": "object"
          },
          "private_terms_hash": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "E400": {
        "description": "Petición incompleta o mal formada (invalid_request)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E401": {
        "description": "Clave ausente, mal formada o revocada (invalid_key, revoked_key)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E403": {
        "description": "Organización suspendida (organization_suspended)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E404": {
        "description": "No encontrado, o certificado que no es de un vocero vigente de la organización (not_found)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E408": {
        "description": "Tiempo agotado al descargar el audio (timeout)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E410": {
        "description": "Certificado revocado (certificate_revoked), con las fechas y el motivo",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E413": {
        "description": "Audio demasiado grande (payload_too_large)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E415": {
        "description": "Formato de audio no admitido (unsupported_media_type)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E422": {
        "description": "El audio no se pudo procesar (invalid_audio)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E429": {
        "description": "Límite alcanzado. quota_exceeded: la organización agotó su cuota del mes (se renueva en resets_at). rate_limited: máximo por hora de la organización (clave live) o de la clave (clave de prueba).",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E500": {
        "description": "Error interno (internal_error)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E502": {
        "description": "El servicio de audio falló (upstream_error)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E503": {
        "description": "El servicio de verificación está atendiendo el máximo de peticiones (capacity_exceeded). No gasta cuota. Reintenta tras Retry-After.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "E409": {
        "description": "Conflicto con el estado actual (p. ej. el titular rechazó este uso)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "description": "Máximo de verificaciones por hora: de la organización con una clave live, de la clave con una de prueba.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Remaining": {
        "description": "Las que quedan en esta hora, contando esta petición.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Segundos hasta que empieza la hora siguiente.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Limit": {
        "description": "Solo claves live: verificaciones al mes de la organización.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Used": {
        "description": "Solo claves live: gastadas este mes, contando esta petición.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Remaining": {
        "description": "Solo claves live: las que quedan este mes.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Reset": {
        "description": "Solo claves live: inicio del mes siguiente (UTC).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "Retry-After": {
        "description": "Segundos que conviene esperar antes de reintentar.",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}
