{
  "openapi": "3.1.0",
  "info": {
    "title": "Vital — API de ingestão de exames",
    "version": "1.0.0",
    "summary": "Recebe exames enviados pelo WhatsApp e entrega acesso ao painel do paciente.",
    "description": "API usada por agentes conversacionais (ex.: GPTMaker) para registrar o consentimento\nLGPD do titular, enviar documentos de exame em qualquer formato, acompanhar o\nprocessamento e entregar o link de acesso ao painel.\n\nTodas as chamadas são assinadas com HMAC-SHA256 sobre `timestamp.corpo`.\nO Vital organiza, explica e encaminha exames; não emite diagnóstico.",
    "contact": {
      "name": "Integrações Vital"
    },
    "license": {
      "name": "Uso restrito a parceiros autorizados"
    }
  },
  "servers": [
    {
      "url": "https://vital.temempresas.com",
      "description": "Servidor atual"
    }
  ],
  "tags": [
    {
      "name": "Consentimento",
      "description": "Portão LGPD obrigatório antes de qualquer arquivo."
    },
    {
      "name": "Ingestão",
      "description": "Envio de exames e acompanhamento da fila."
    },
    {
      "name": "Acesso",
      "description": "Link temporário para o painel do paciente."
    },
    {
      "name": "Direitos do titular",
      "description": "Portabilidade e eliminação (LGPD art. 18)."
    }
  ],
  "security": [
    {
      "VitalSignature": [],
      "VitalTimestamp": []
    }
  ],
  "paths": {
    "/api/public/v1/consents": {
      "post": {
        "tags": [
          "Consentimento"
        ],
        "summary": "Registra aceite ou revogação do termo",
        "description": "Sem um aceite ativo aqui, nenhum documento é aceito em `/documents`. Envie `accepted: false` para revogar.",
        "operationId": "recordConsent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConsentRequest"
              },
              "examples": {
                "aceite": {
                  "summary": "Aceite do termo",
                  "value": {
                    "patient": {
                      "phone": "+5511999998888",
                      "full_name": "Maria Souza",
                      "cpf": "12345678909"
                    },
                    "accepted": true,
                    "term_version": "v1.0",
                    "term_summary": "Resumo do termo apresentado ao titular.",
                    "channel": "whatsapp",
                    "evidence": {
                      "wamid": "wamid.EXEMPLO",
                      "message_text": "ACEITO"
                    }
                  }
                },
                "revogacao": {
                  "summary": "Revogação",
                  "value": {
                    "patient": {
                      "phone": "+5511999998888"
                    },
                    "accepted": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Consentimento registrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsentResponse"
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/documents": {
      "post": {
        "tags": [
          "Ingestão"
        ],
        "summary": "Envia um exame para a fila de leitura",
        "description": "Aceita `url` (o arquivo é baixado na hora, pois a URL do WhatsApp expira), `base64` ou `text`. Tipos aceitos: PDF, JPEG, PNG, WEBP, HEIC e texto puro. Limite de 100 MB por arquivo. Reenvios com o mesmo `idempotency_key` — ou um arquivo idêntico já recebido — retornam `200` com `duplicated: true`.",
        "operationId": "submitDocument",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentRequest"
              },
              "examples": {
                "pdfPorUrl": {
                  "summary": "PDF por URL",
                  "value": {
                    "idempotency_key": "wamid.EXEMPLO",
                    "patient": {
                      "phone": "+5511999998888"
                    },
                    "batch_id": "lote-2026-08-28",
                    "source": "whatsapp",
                    "media": {
                      "type": "application/pdf",
                      "url": "https://exemplo.test/hemograma.pdf",
                      "filename": "hemograma.pdf",
                      "caption": "exame de ontem"
                    }
                  }
                },
                "laudoEmTexto": {
                  "summary": "Laudo colado como texto",
                  "value": {
                    "patient": {
                      "phone": "+5511999998888"
                    },
                    "media": {
                      "type": "text/plain",
                      "text": "Hemoglobina 13,4 g/dL (12,0 - 15,5)"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reentrega ou arquivo idêntico já recebido.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "duplicated": {
                      "type": "boolean",
                      "const": true
                    },
                    "document_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Documento aceito e enfileirado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "document_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "examples": [
                        "received"
                      ]
                    },
                    "queued": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`consent_required` — peça o aceite do termo antes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Arquivo acima do limite de 100 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Tipo de mídia não aceito.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/status": {
      "get": {
        "tags": [
          "Ingestão"
        ],
        "summary": "Situação do lote do titular",
        "description": "Devolve apenas contagens e o estado do consentimento — nenhum dado clínico ou identificador pessoal. Para este endpoint, o conteúdo assinado é a query string, incluindo o `?`.",
        "operationId": "getStatus",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 10,
              "maxLength": 20,
              "description": "Telefone do titular em E.164.",
              "examples": [
                "+5511999998888"
              ]
            },
            "description": "Telefone do titular em E.164 (codificado na URL)."
          }
        ],
        "responses": {
          "200": {
            "description": "Contagens dos documentos recentes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`patient_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/process": {
      "post": {
        "tags": [
          "Ingestão"
        ],
        "summary": "Executa a fila de processamento",
        "description": "Processa até 3 documentos por chamada (triagem → extração → normalização → consolidação → interpretação → publicação). Cadência recomendada: 1 chamada por minuto.",
        "operationId": "processQueue",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado do lote processado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "processed": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "document_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "status": {
                            "type": "string",
                            "examples": [
                              "ready",
                              "failed",
                              "rejected"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/magic-link": {
      "post": {
        "tags": [
          "Acesso"
        ],
        "summary": "Gera o link de acesso ao painel",
        "description": "Link de uso único, válido por 15 minutos.",
        "operationId": "createMagicLink",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MagicLinkRequest"
              },
              "example": {
                "patient": {
                  "phone": "+5511999998888"
                },
                "redirect_path": "/painel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link emitido.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in": {
                      "type": "integer",
                      "examples": [
                        900
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`patient_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/erasure": {
      "post": {
        "tags": [
          "Direitos do titular"
        ],
        "summary": "Portabilidade e eliminação de dados",
        "description": "`export` devolve os dados do titular; `delete` agenda a eliminação com 7 dias de carência; `cancel` cancela a eliminação agendada. Para revogar consentimento use `/consents` com `accepted: false`.",
        "operationId": "handleDataSubjectRequest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ErasureRequest"
              },
              "example": {
                "patient": {
                  "phone": "+5511999998888"
                },
                "action": "delete"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Para `export`, o pacote de dados do titular. Para `delete`/`cancel`, a confirmação com a data prevista.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido segundo o schema do endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Assinatura ausente, inválida ou fora da janela de tolerância.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`patient_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "VitalSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "x-vital-signature",
        "description": "HMAC-SHA256 de `${timestamp}.${corpo}` em hexadecimal minúsculo, usando o segredo compartilhado com o parceiro."
      },
      "VitalTimestamp": {
        "type": "apiKey",
        "in": "header",
        "name": "x-vital-timestamp",
        "description": "Epoch em segundos. Janela de tolerância de 5 minutos."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "invalid_payload"
            ]
          }
        }
      },
      "PatientRef": {
        "type": "object",
        "required": [
          "phone"
        ],
        "properties": {
          "phone": {
            "type": "string",
            "minLength": 10,
            "maxLength": 20,
            "description": "Telefone do titular em E.164.",
            "examples": [
              "+5511999998888"
            ]
          }
        }
      },
      "ConsentRequest": {
        "type": "object",
        "required": [
          "patient",
          "accepted"
        ],
        "properties": {
          "event": {
            "type": "string",
            "maxLength": 64
          },
          "patient": {
            "type": "object",
            "required": [
              "phone"
            ],
            "properties": {
              "phone": {
                "type": "string",
                "minLength": 10,
                "maxLength": 20,
                "description": "Telefone do titular em E.164.",
                "examples": [
                  "+5511999998888"
                ]
              },
              "full_name": {
                "type": "string",
                "maxLength": 160
              },
              "cpf": {
                "type": "string",
                "maxLength": 20,
                "description": "Somente dígitos; validado."
              },
              "birth_date": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "accepted": {
            "type": "boolean",
            "description": "`false` registra a revogação."
          },
          "term_version": {
            "type": "string",
            "maxLength": 32,
            "examples": [
              "v1.0"
            ]
          },
          "term_summary": {
            "type": "string",
            "maxLength": 4000
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "app",
              "web"
            ]
          },
          "evidence": {
            "type": "object",
            "properties": {
              "wamid": {
                "type": "string",
                "maxLength": 200
              },
              "message_text": {
                "type": "string",
                "maxLength": 500
              },
              "accepted_at": {
                "type": "string",
                "maxLength": 40
              }
            }
          }
        }
      },
      "ConsentResponse": {
        "type": "object",
        "properties": {
          "consent_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "accepted",
              "revoked"
            ]
          },
          "term_version": {
            "type": "string",
            "examples": [
              "v1.0"
            ]
          }
        }
      },
      "DocumentRequest": {
        "type": "object",
        "required": [
          "patient",
          "media"
        ],
        "properties": {
          "event": {
            "type": "string",
            "maxLength": 64
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 6,
            "maxLength": 200,
            "description": "Recomendado: o identificador da mensagem do WhatsApp."
          },
          "patient": {
            "type": "object",
            "required": [
              "phone"
            ],
            "properties": {
              "phone": {
                "type": "string",
                "minLength": 10,
                "maxLength": 20,
                "description": "Telefone do titular em E.164.",
                "examples": [
                  "+5511999998888"
                ]
              }
            }
          },
          "consent_id": {
            "type": "string",
            "format": "uuid"
          },
          "batch_id": {
            "type": "string",
            "maxLength": 120
          },
          "source": {
            "type": "string",
            "enum": [
              "whatsapp",
              "app",
              "web"
            ]
          },
          "media": {
            "type": "object",
            "required": [
              "type"
            ],
            "description": "Informe exatamente um entre `url`, `base64` e `text`.",
            "properties": {
              "type": {
                "type": "string",
                "description": "MIME do arquivo.",
                "examples": [
                  "application/pdf",
                  "image/jpeg",
                  "image/png",
                  "text/plain"
                ]
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2000
              },
              "base64": {
                "type": "string",
                "description": "Arquivo inline em base64."
              },
              "text": {
                "type": "string",
                "maxLength": 200000
              },
              "filename": {
                "type": "string",
                "maxLength": 200
              },
              "caption": {
                "type": "string",
                "maxLength": 1000
              }
            }
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "properties": {
          "consent": {
            "type": "string",
            "enum": [
              "accepted",
              "revoked",
              "missing"
            ]
          },
          "ready": {
            "type": "integer"
          },
          "processing": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          }
        }
      },
      "MagicLinkRequest": {
        "type": "object",
        "required": [
          "patient"
        ],
        "properties": {
          "patient": {
            "type": "object",
            "required": [
              "phone"
            ],
            "properties": {
              "phone": {
                "type": "string",
                "minLength": 10,
                "maxLength": 20,
                "description": "Telefone do titular em E.164.",
                "examples": [
                  "+5511999998888"
                ]
              }
            }
          },
          "redirect_path": {
            "type": "string",
            "maxLength": 120,
            "pattern": "^/[A-Za-z0-9\\-_/]*$",
            "examples": [
              "/painel"
            ]
          }
        }
      },
      "ErasureRequest": {
        "type": "object",
        "required": [
          "patient",
          "action"
        ],
        "properties": {
          "patient": {
            "type": "object",
            "required": [
              "phone"
            ],
            "properties": {
              "phone": {
                "type": "string",
                "minLength": 10,
                "maxLength": 20,
                "description": "Telefone do titular em E.164.",
                "examples": [
                  "+5511999998888"
                ]
              }
            }
          },
          "action": {
            "type": "string",
            "enum": [
              "export",
              "delete",
              "cancel"
            ]
          },
          "evidence": {
            "type": "object",
            "properties": {
              "wamid": {
                "type": "string",
                "maxLength": 200
              }
            }
          }
        }
      }
    }
  }
}