Janelas de Tempo de Acesso

As Access Time Windows permitem que um membro de parceiro tenha acesso temporário ou recorrente fornecido por um sistema externo. Esta página é o guia oficial para produtores de access_time_windows e access_time_windows_revision.

As Access Time Windows são permissões adicionais que apenas concedem acesso. Uma janela ativa pode permitir o acesso, mas uma janela inativa, expirada, desatualizada ou não mapeada não nega um acesso que uma regra regular ou uma concessão permanente já permite. A desativação de usuários, o bloqueio explícito de portas, o modo de manutenção, a geocerca, os limites de taxa e a disponibilidade do controlador continuam valendo.

As implementações para sistemas de origem específicos são adaptadores separados. Elas devem converter os dados de origem para este contrato independente de fornecedor sem alterar o significado dos campos.

Campos do snapshot do membro

  • access_time_windows array opcional

    O snapshot completo de Access Time Windows deste membro e desta origem de parceiro. Um snapshot pode combinar janelas absolute e weekly. Omita o campo para manter o snapshot armazenado inalterado; envie [] para revogar todas as janelas pertencentes a essa origem.

  • access_time_windows_revision integer obrigatório com access_time_windows

    Um inteiro não negativo, monotonicamente crescente e seguro para JSON, para este fluxo de revisões de membro/origem. É obrigatório sempre que access_time_windows estiver presente e não pode exceder 9007199254740991.

Cada snapshot aceita no máximo 50 janelas. external_id, scope_key e label aceitam no máximo 120 caracteres. As janelas absolutas podem abranger no máximo 90 dias. Um dia semanal aceita no máximo oito blocos sem sobreposição. Dados inválidos são rejeitados, e não truncados ou presumidos.

Formatos de janela

Uma janela absoluta identifica um intervalo exato:

{
  "external_id": "access-4b1782",
  "kind": "absolute",
  "from": 1792053000,
  "to": 1792058400,
  "timezone": "Australia/Perth",
  "scope_key": "after_hours_entry",
  "label": "Temporary site access"
}

Os campos obrigatórios são external_id, kind, from e to. timezone, scope_key e label são opcionais.

Uma janela semanal identifica intervalos recorrentes no horário local, opcionalmente limitados por instantes em UTC:

{
  "external_id": "weekly-early-access",
  "kind": "weekly",
  "timezone": "America/New_York",
  "access_hours": {
    "sun": [],
    "mon": [["06:00", "08:30"]],
    "tue": [["06:00", "08:30"]],
    "wed": [["06:00", "08:30"]],
    "thu": [["06:00", "08:30"]],
    "fri": [["06:00", "08:30"]],
    "sat": []
  },
  "active_from": 1790812800,
  "active_until": 1796083200,
  "scope_key": "main_entry",
  "label": "Weekday early access"
}

Os campos obrigatórios são external_id, kind e access_hours. timezone, active_from, active_until, scope_key e label são opcionais.

Solicitação combinada completa

Esta solicitação substitui o snapshot anterior do membro pertencente à origem por uma janela absoluta e uma janela semanal:

curl -X POST https://partner-api.performancehub.co/partner/v1/members \
  -u sk_live_YOUR_PARTNER_SECRET_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "members": [
      {
        "member_id": "member_11111",
        "facility_id": "550e8400-e29b-41d4-a716-446655440000",
        "first_name": "John",
        "last_name": "Smith",
        "access_time_windows_revision": 1842,
        "access_time_windows": [
          {
            "external_id": "access-4b1782",
            "kind": "absolute",
            "from": 1792053000,
            "to": 1792058400,
            "timezone": "Australia/Perth",
            "scope_key": "after_hours_entry",
            "label": "Temporary site access"
          },
          {
            "external_id": "weekly-early-access",
            "kind": "weekly",
            "timezone": "America/New_York",
            "access_hours": {
              "sun": [],
              "mon": [["06:00", "08:30"]],
              "tue": [["06:00", "08:30"]],
              "wed": [["06:00", "08:30"]],
              "thu": [["06:00", "08:30"]],
              "fri": [["06:00", "08:30"]],
              "sat": []
            },
            "active_from": 1790812800,
            "active_until": 1796083200,
            "scope_key": "main_entry",
            "label": "Weekday early access"
          }
        ]
      }
    ]
  }'

Use facility_id ou linking_identifier, de acordo com o método de vinculação configurado. Não envie os dois.

Regras de horário e limites

from, to, active_from e active_until são inteiros JSON em segundos Unix que identificam instantes em UTC:

  • Envie um inteiro JSON sem aspas. Strings numéricas, números de ponto flutuante, strings RFC 3339 e milissegundos Unix são rejeitados.
  • Todo intervalo é semiaberto: [from, to). O acesso pode começar exatamente em from e deixa de estar ativo exatamente em to.
  • from deve ser menor que to. Quando os dois limites semanais estiverem presentes, active_from deve ser menor que active_until.
  • Não derive épocas de uma data/hora local sem fuso horário. Primeiro, converta o horário de origem em um instante UTC, depois divida os milissegundos Unix por 1000 e verifique se o resultado é um inteiro.

Conversão de época

2026-10-15T08:30:00Z é convertido em 1792053000:

UTC instant                 Unix milliseconds   Unix seconds sent
2026-10-15T08:30:00Z        1792053000000        1792053000

Conversão independente de linguagem:

instant = parse_source_timestamp_with_explicit_zone(source_timestamp)
milliseconds = instant.to_unix_milliseconds()
assert milliseconds modulo 1000 equals 0
seconds = milliseconds / 1000
assert seconds is a JSON-safe nonnegative integer

Se a origem tiver precisão inferior a um segundo, defina uma política deliberada de arredondamento de início/fim antes dos testes de integração. Nunca envie milissegundos esperando que a API deduza a unidade.

Fusos horários e horário de verão

timezone deve ser um fuso horário IANA válido, como UTC, Australia/Perth ou America/New_York. O padrão é UTC quando omitido.

  • Janelas absolutas: from e to já identificam instantes UTC exatos. timezone é apenas um metadado de exibição e auditoria; alterá-lo nunca desloca nenhum dos instantes.
  • Janelas semanais: timezone define o horário local e as regras de horário de verão usadas para avaliar access_hours. Não converta previamente os blocos semanais para UTC.
  • Durante uma mudança em que o relógio é adiantado, um horário local que o fuso horário pula nunca ocorre.
  • Durante uma mudança em que o relógio é atrasado, os dois instantes reais com o horário local repetido correspondem ao bloco semanal.
  • active_from e active_until continuam sendo instantes UTC, mesmo quando uma janela semanal usa um fuso horário diferente de UTC.

access_hours usa sun a sat em minúsculas. Cada valor é uma matriz de pares [start, end] no formato HH:mm de 24 horas, com zeros à esquerda. Os blocos não podem ser vazios, devem estar ordenados e não podem se sobrepor. end é exclusivo. Um bloco não pode cruzar a meia-noite; divida um intervalo noturno de origem em um bloco no fim do primeiro dia e um bloco no início do dia seguinte.

IDs estáveis e mapeamento de escopo

external_id deve ser estável durante toda a vida de uma janela lógica e exclusivo dentro do snapshot do membro. Mantenha-o estável quando horários ou rótulos mudarem, para que os operadores e os registros de auditoria possam correlacionar as revisões. Não o reutilize para uma janela não relacionada.

scope_key é o seu destino lógico e estável de acesso. O padrão é default. Um administrador da instalação mapeia cada chave permitida para destinos do Performance Hub de toda a instalação, de portas ou de Escopo de acesso:

Partner scope_key           Performance Hub mapping
default                     Facility's configured default target
main_entry                  Access Scope "Main entrances"
after_hours_entry           Doors approved for after-hours entry

Os parceiros não enviam IDs de instalação, porta ou Escopo de acesso do Performance Hub dentro de uma janela. Um scope_key desconhecido, desativado ou não mapeado falha de forma fechada para essa janela; ele não amplia o acesso. Combine e teste os mapeamentos antes do lançamento.

Snapshots completos e revisões

Trate os dois campos como uma partição com versionamento próprio dentro do upsert de membro:

EntradaResultado
access_time_windows omitidoO snapshot existente permanece inalterado; nenhuma revisão é necessária
Presente com uma revisão mais novaTodo o snapshot armazenado para este membro/origem é substituído
Mesma revisão e mesmo payload normalizadoRepetição idempotente; o snapshot não muda
Mesma revisão e payload normalizado diferenteConflito; atribua uma nova revisão após a reconciliação
Revisão mais antigaO snapshot desatualizado é ignorado; ele não pode substituir um estado mais recente
access_time_windows: [] com uma revisão mais novaRevoga explicitamente todas as janelas pertencentes à origem

Um snapshot é completo, não um patch. Se a revisão 1842 tem as janelas A e B, a revisão 1843 contendo apenas B remove A.

Mantenha um único fluxo de revisões coordenado por parceiro/membro entre workers de webhook, polling, repetição e reconciliação completa. Uma sequência durável de banco de dados é preferível. Um valor em milissegundos Unix só pode ser usado como revisão quando todos os gravadores se coordenam e garantem ordenação e exclusividade estritas; ele é um valor de revisão, não um campo de época.

Mantenha a solicitação e a revisão exatas até que a resposta da API seja conhecida. Após um tempo limite ou erro transitório, repita o mesmo snapshot normalizado com a mesma revisão. Não atribua uma nova revisão apenas porque a entrega pelo transporte é incerta.

Cancelamento, expiração e falhas na origem

  • Cancelar uma janela: consulte com sucesso o estado oficial da origem, remova essa janela e envie o snapshot completo restante com uma revisão mais nova.
  • Cancelar todas as janelas: envie access_time_windows: [] com uma revisão mais nova.
  • Deixar uma janela absoluta expirar: ela deixa de permitir acesso em to; envie uma reconciliação posterior sem a entrada expirada para manter os snapshots limpos.
  • Encerrar uma janela semanal: remova-a em um snapshot mais novo ou use active_until quando a origem tiver um instante de término conhecido.
  • A consulta à origem falha ou está incompleta: não envie [] e não publique um snapshot parcial. Emita um alerta e repita a consulta. Transformar uma falha na origem em um snapshot vazio é uma revogação de acesso não intencional.
  • A solicitação à Partner API falha na validação: corrija os dados e envie uma revisão mais nova somente se o snapshot normalizado mudar. Repita falhas transitórias com a revisão original.

A atualização dos dados de origem é configurada por integração, com base na cadência de entrega e de reconciliação prometida. Antes da ativação, combine a idade máxima dos dados de origem, o que acontece quando ela é excedida, os limites de alerta e se os operadores podem aceitar depender apenas dos limites de cada janela. Uma janela de origem desatualizada nunca se torna uma regra de negação; as regras regulares e as concessões permanentes continuam sendo caminhos alternativos.

Algoritmo do produtor

Use este fluxo independente de linguagem em toda atualização de membro:

function publish_member(member_key):
    source_result = fetch_authoritative_access_state(member_key)

    if source_result.failed or source_result.incomplete:
        record_failure(member_key)
        schedule_retry(member_key)
        return without sending access_time_windows

    windows = []
    for source_window in source_result.current_windows:
        if source_window.is_absolute:
            windows.append({
                external_id: stable_window_id(source_window),
                kind: "absolute",
                from: to_integer_unix_seconds(source_window.start_instant),
                to: to_integer_unix_seconds(source_window.end_instant),
                timezone: source_window.display_timezone or "UTC",
                scope_key: stable_scope_key(source_window) or "default",
                label: safe_label(source_window)
            })
        else:
            windows.append({
                external_id: stable_window_id(source_window),
                kind: "weekly",
                timezone: valid_iana_zone(source_window.timezone) or "UTC",
                access_hours: split_validate_and_sort_weekly_blocks(source_window),
                active_from: optional_integer_unix_seconds(source_window.active_start),
                active_until: optional_integer_unix_seconds(source_window.active_end),
                scope_key: stable_scope_key(source_window) or "default",
                label: safe_label(source_window)
            })

    normalized_snapshot = validate_sort_and_deduplicate(windows)
    revision = next_durable_member_revision(member_key)
    request = member_upsert_with(normalized_snapshot, revision)
    persist_for_retry(member_key, request)

    response = post_request(request)
    if response.is_transient_or_unknown:
        retry_exact_request_with_same_revision(request)
    else if response.is_revision_conflict:
        reconcile_with_latest_committed_source_state(member_key)
    else if response.is_stale_skip:
        stop_replaying_this_revision_and_reconcile(member_key)

Lista de verificação de certificação

Conclua estas verificações em uma instalação de staging, usando o mapeamento final de scope_key:

  • Uma janela absoluta é aceita com o fuso horário omitido e é avaliada em UTC.
  • Uma janela absoluta com fuso horário IANA personalizado preserva os mesmos instantes from/to.
  • Uma janela semanal com fuso horário IANA personalizado segue o horário local nas transições de adiantamento e atraso do relógio.
  • Um snapshot combinado absoluto/semanal substitui completamente o snapshot anterior.
  • O acesso começa exatamente em from e termina exatamente em to; os fins dos blocos semanais também são exclusivos.
  • A atualização de horários sob o mesmo external_id com uma revisão mais nova entra em vigor.
  • Omitir access_time_windows altera outros campos do membro sem alterar as janelas.
  • Repetir a mesma revisão/payload é idempotente.
  • Mesma revisão/payload diferente retorna um conflito e não altera o snapshot.
  • Uma revisão mais antiga fora de ordem é relatada como desatualizada e não pode reverter o estado.
  • Um [] mais novo revoga as janelas pertencentes à origem; uma falha na consulta à origem não envia nada.
  • Um scope_key desconhecido ou não mapeado falha de forma fechada e gera um aviso visível para o operador.
  • Os alertas de idade dos dados de origem e a política configurada para origem desatualizada funcionam no limite acordado.
  • Os caminhos de webhook e de reconciliação completa convergem para o mesmo snapshot e a mesma ordem de revisões.
  • A entrega pelo fluxo chega ao Door & Gate Access dentro da meta acordada.
  • Uma janela ativa pode permitir a porta física mapeada, mas não pode contornar a desativação de usuários, blockAllAccess, a manutenção ou a geocerca.
  • Antes, fora e depois de uma janela, as regras regulares e as concessões permanentes continuam permitindo os usuários que se qualificam de outra forma.

Vetores de teste mínimos

[
  {
    "name": "absolute-half-open",
    "window": {
      "external_id": "tv-absolute",
      "kind": "absolute",
      "from": 1792053000,
      "to": 1792058400
    },
    "expect": {
      "1792052999": "inactive",
      "1792053000": "active",
      "1792058399": "active",
      "1792058400": "inactive"
    }
  },
  {
    "name": "weekly-dst",
    "window": {
      "external_id": "tv-weekly-dst",
      "kind": "weekly",
      "timezone": "America/New_York",
      "access_hours": {
        "sun": [["01:00", "03:30"]],
        "mon": [],
        "tue": [],
        "wed": [],
        "thu": [],
        "fri": [],
        "sat": []
      }
    },
    "expect": "Evaluate local wall time: skipped times do not occur; both repeated 01:30 instants qualify."
  }
]

Envie também vetores negativos para milissegundos Unix, strings numéricas, números de ponto flutuante, fusos IANA inválidos, from >= to, external_id duplicado, blocos sobrepostos, blocos noturnos, mais de oito blocos em um dia, mais de 50 janelas e chaves de escopo não mapeadas.