Datas em APIs precisam representar o que o dado significa. Um pagamento confirmado é um instante na linha do tempo; um aniversário é uma data do calendário; uma consulta marcada para as 9h é um compromisso que pode depender do horário de uma cidade. Converter os três para uma meia-noite em UTC produz erros diferentes com a mesma aparência de padronização.
UTC é útil para comparar instantes entre sistemas. Ele não substitui a definição do campo, nem conserva sozinho a intenção de todo agendamento. Antes de escolher um formato, decida qual dessas informações o consumidor precisa preservar.
Neste artigo
Três perguntas para definir o campo
| Informação | Pergunta que precisa ser respondida | Representação de exemplo |
|---|---|---|
| Registro de uma ocorrência | Em que instante aconteceu? | 2026-11-03T12:00:00Z |
| Data civil | Qual dia do calendário foi informado? | 1990-05-12 |
| Compromisso futuro | Em que horário local e em qual região deve ocorrer? | Data e hora locais mais America/Fortaleza |
No primeiro caso, qualquer sistema pode converter o instante para apresentar a hora local ao leitor. No segundo, aplicar um fuso a um aniversário inventa uma informação que o dado original não tinha. No terceiro, só o instante calculado pode ser insuficiente para explicar ou recalcular o compromisso se as regras do fuso mudarem.
Essa semântica deve fazer parte do contrato da API tanto quanto o tipo do campo e sua obrigatoriedade. Um valor que passa no parser ainda pode representar a coisa errada.
O mesmo instante pode ter três horários corretos
Considere 2026-11-03T09:00:00-03:00. O sufixo informa que o horário está três horas atrás de UTC. O instante correspondente é 2026-11-03T12:00:00Z. Naquela data, sua apresentação em Berlim é 13h.
O RFC 3339 define um formato de timestamp usado em protocolos da internet, com data, hora e indicação do deslocamento em relação a UTC. Z representa UTC; um offset como -03:00 permite obter o mesmo instante por outra apresentação.
O exemplo abaixo foi executado com datetime e zoneinfo do Python:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
origem = datetime.fromisoformat("2026-11-03T09:00:00-03:00")
print(origem.astimezone(timezone.utc).isoformat())
print(origem.astimezone(ZoneInfo("Europe/Berlin")).isoformat())
2026-11-03T12:00:00+00:00
2026-11-03T13:00:00+01:00
Os três relógios indicam a mesma ocorrência. Somar três horas ao valor e conservar -03:00 seria mudar o instante, não apenas sua exibição.
Offset não é identificador de fuso
-03:00 informa uma diferença numérica para UTC. America/Fortaleza identifica uma região e as regras de horário associadas a ela na base de fusos. Mesmo que coincidam agora, duas regiões podem ter históricos e regras futuras diferentes.
O RFC 9557 amplia o tratamento de timestamps para permitir informação adicional de fuso. Isso não significa que toda biblioteca ou API aceite automaticamente sua sintaxe. Uma alternativa explícita no contrato é enviar campos separados para o horário local e o identificador regional.
Para um compromisso marcado pelas pessoas em horário local, o registro pode conter:
{
"data_hora_local": "2027-04-12T09:00:00",
"fuso": "America/Fortaleza",
"instante_calculado": "2027-04-12T12:00:00Z"
}
O exemplo é um desenho de contrato, não uma exigência universal. É necessário definir qual campo é a referência quando houver divergência: a intenção de acontecer às 9h locais ou o instante UTC já contratado. Alterar um agendamento sem essa regra pode deslocá-lo silenciosamente.
Uma hora local pode ocorrer duas vezes
Mudanças de horário também criam valores ambíguos. Na base de fusos usada na demonstração, 25/10/2026 às 02h30 em Europe/Berlin corresponde a duas ocorrências: uma com offset +02:00 e outra com +01:00.
| Hora local informada | Offset escolhido | Instante UTC |
|---|---|---|
| 25/10/2026, 02h30 | +02:00 |
00:30Z |
| 25/10/2026, 02h30 | +01:00 |
01:30Z |
A documentação de zoneinfo explica o atributo fold, que permite distinguir ocorrências repetidas. Em outros ajustes de relógio, há horários locais que sequer existem. Aceitar uma string não demonstra que ela seja válida naquele fuso e naquela data.
Uma API que recebe agendamentos precisa decidir como rejeita ou resolve esses casos. Para uma operação relevante, pedir uma escolha explícita é preferível a normalizar o horário sem avisar o usuário.
O banco pode guardar o instante sem guardar o fuso original
No PostgreSQL, timestamp with time zone, também chamado timestamptz, representa um instante convertido internamente para UTC e apresentado conforme o fuso da sessão. Ele não conserva o nome do fuso originalmente informado. Já timestamp without time zone não carrega essa interpretação temporal por conta própria, conforme a documentação de tipos de data e hora.
Por isso, escolher timestamptz é apropriado para muitos registros de ocorrências, mas não elimina um campo adicional quando a aplicação precisa lembrar a região escolhida pelo usuário. Também é preciso conferir como o driver serializa os valores ao devolvê-los.
O que testar antes de liberar o contrato
Comece com valores cujo resultado seja conhecido: o mesmo instante recebido com offsets diferentes; uma data de nascimento que deve continuar no mesmo dia; um agendamento acompanhado de identificador regional; e um horário repetido ou inexistente.
Confira a ida e a volta pelo banco e pelo cliente. O teste deve observar o valor entregue ao usuário, não apenas comparar strings. Dois timestamps podem apontar para o mesmo instante e ter textos diferentes; duas datas textualmente iguais podem estar sendo interpretadas em regiões diferentes.
Para fechar o contrato, registre o significado do campo, o formato aceito, a exigência de offset ou fuso, a regra para valores ambíguos e quem pode recalcular um compromisso. Assim, o próximo consumidor consegue saber se deve preservar um instante, uma data civil ou a intenção de um horário local.