Plages horaires d'accès

Les Access Time Windows permettent à un membre partenaire de bénéficier d'un accès temporaire ou récurrent fourni par un système externe. Cette page est le guide de référence pour les producteurs de access_time_windows et de access_time_windows_revision.

Les Access Time Windows sont des autorisations supplémentaires qui ne peuvent qu'accorder l'accès. Une plage active peut autoriser l'accès, mais une plage inactive, expirée, obsolète ou sans correspondance ne refuse pas un accès déjà autorisé par une règle habituelle ou une autorisation permanente. La désactivation d'un utilisateur, le blocage explicite d'une porte, le mode de maintenance, le géorepérage, les limites de débit et la disponibilité du contrôleur continuent de s'appliquer.

Les implémentations destinées à des systèmes en amont particuliers sont des adaptateurs distincts. Elles doivent traduire leurs données sources dans ce contrat indépendant des fournisseurs sans modifier la signification de ses champs.

Champs de l'instantané de membre

  • access_time_windows array facultatif

    L'instantané complet des Access Time Windows pour ce membre et cette source partenaire. Un instantané peut combiner des plages absolute et weekly. Omettez le champ pour conserver l'instantané stocké tel quel ; envoyez [] pour révoquer toutes les plages appartenant à cette source.

  • access_time_windows_revision integer obligatoire avec access_time_windows

    Un entier non négatif, strictement croissant et compatible JSON, propre au flux de révisions de ce membre/cette source. Il est obligatoire dès que access_time_windows est présent et ne doit pas dépasser 9007199254740991.

Chaque instantané accepte au maximum 50 plages. external_id, scope_key et label acceptent au maximum 120 caractères. Une plage absolue peut couvrir au maximum 90 jours. Un jour de la semaine accepte au maximum huit blocs sans chevauchement. Les données invalides sont rejetées au lieu d'être tronquées ou interprétées.

Formes des plages

Une plage absolue désigne un intervalle exact unique :

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

Les champs obligatoires sont external_id, kind, from et to. timezone, scope_key et label sont facultatifs.

Une plage hebdomadaire désigne des intervalles récurrents en heure locale, éventuellement bornés par des instants 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"
}

Les champs obligatoires sont external_id, kind et access_hours. timezone, active_from, active_until, scope_key et label sont facultatifs.

Requête mixte complète

Cette requête remplace l'instantané précédent du membre appartenant à la source par une plage absolue et une plage hebdomadaire :

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"
          }
        ]
      }
    ]
  }'

Utilisez facility_id ou linking_identifier selon la méthode de liaison configurée. N'envoyez pas les deux.

Règles relatives aux heures et aux limites

from, to, active_from et active_until sont des entiers JSON exprimés en secondes Unix, qui désignent des instants UTC :

  • Envoyez un entier JSON sans guillemets. Les chaînes numériques, les nombres à virgule flottante, les chaînes RFC 3339 et les millisecondes Unix sont rejetés.
  • Chaque intervalle est semi-ouvert : [from, to). L'accès peut commencer exactement à from ; il n'est plus actif exactement à to.
  • from doit être inférieur à to. Lorsque les deux bornes hebdomadaires sont présentes, active_from doit être inférieur à active_until.
  • Ne calculez pas d'epochs à partir d'une date/heure locale sans fuseau horaire. Convertissez d'abord l'heure source en instant UTC, puis divisez les millisecondes Unix par 1000 et vérifiez que le résultat est un entier.

Conversion en epoch

2026-10-15T08:30:00Z est converti en 1792053000 :

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

Conversion indépendante du langage :

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

Si la source offre une précision inférieure à la seconde, définissez une politique d'arrondi explicite pour le début et la fin avant les tests d'intégration. Ne transmettez jamais de millisecondes en comptant sur l'API pour deviner l'unité.

Fuseaux horaires et heure d'été

timezone doit être un fuseau horaire IANA valide, comme UTC, Australia/Perth ou America/New_York. Sa valeur par défaut est UTC lorsqu'il est omis.

  • Plages absolues : from et to désignent déjà des instants UTC exacts. timezone n'est qu'une métadonnée d'affichage et d'audit ; la modifier ne décale jamais aucun des deux instants.
  • Plages hebdomadaires : timezone définit l'heure locale et les règles d'heure d'été utilisées pour évaluer access_hours. Ne convertissez pas les blocs hebdomadaires en UTC au préalable.
  • Lors du passage à l'heure d'été, une heure locale sautée par le fuseau horaire ne se produit jamais.
  • Lors du retour à l'heure d'hiver, les deux instants réels correspondant à l'heure locale répétée correspondent au bloc hebdomadaire.
  • active_from et active_until restent des instants UTC, même lorsqu'une plage hebdomadaire utilise un fuseau horaire autre qu'UTC.

access_hours utilise les valeurs en minuscules sun à sat. Chaque valeur est un tableau de paires [start, end] au format HH:mm sur 24 heures, complété par des zéros. Les blocs doivent être non vides, ordonnés et sans chevauchement. end est exclusif. Un bloc ne peut pas franchir minuit ; scindez un intervalle source de nuit en un bloc tardif le premier jour et un bloc matinal le jour suivant.

ID stables et correspondance des portées

external_id doit rester stable pendant toute la durée de vie d'une plage logique et être unique au sein de l'instantané du membre. Conservez-le lorsque les heures ou les libellés changent, afin que les opérateurs et les enregistrements d'audit puissent rapprocher les révisions. Ne le réutilisez pas pour une plage sans rapport.

scope_key est votre cible d'accès logique stable. Sa valeur par défaut est default. Un administrateur de l'installation associe chaque clé autorisée à des cibles Performance Hub : l'ensemble de l'installation, une porte ou une Portée d'accès :

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

Les partenaires n'envoient pas d'ID d'installation, de porte ou de Portée d'accès Performance Hub dans une plage. Un scope_key inconnu, désactivé ou sans correspondance entraîne un refus par défaut pour cette plage ; il n'élargit pas l'accès. Convenez des correspondances et testez-les avant le lancement.

Instantanés complets et révisions

Traitez ces deux champs comme une partition versionnée séparément au sein de l'upsert de membre :

EntréeRésultat
access_time_windows omisL'instantané existant reste inchangé ; aucune révision n'est requise
Présent avec une révision plus récenteL'intégralité de l'instantané stocké pour ce membre/cette source est remplacée
Même révision et même charge utile normaliséeRelecture idempotente ; aucune modification de l'instantané
Même révision et charge utile normalisée différenteConflit ; attribuez une nouvelle révision après rapprochement
Révision plus ancienneL'instantané obsolète est ignoré ; il ne peut pas écraser un état plus récent
access_time_windows: [] avec une révision plus récenteRévoque explicitement toutes les plages appartenant à la source

Un instantané est complet, ce n'est pas un correctif partiel. Si la révision 1842 contient les plages A et B, alors la révision 1843 qui ne contient que B supprime A.

Maintenez un flux de révisions coordonné unique par partenaire/membre pour l'ensemble des processus de webhook, d'interrogation périodique, de relecture et de rapprochement complet. Une séquence de base de données durable est préférable. Une valeur en millisecondes Unix ne peut servir de révision que si tous les processus d'écriture se coordonnent et garantissent un ordre strict et l'unicité ; il s'agit d'une valeur de révision, et non d'un champ d'epoch.

Conservez la requête et la révision exactes jusqu'à ce que la réponse de l'API soit connue. Après un délai d'expiration ou une erreur temporaire, renvoyez le même instantané normalisé avec la même révision. N'attribuez pas une nouvelle révision au seul motif que la livraison est incertaine.

Annulation, expiration et échecs en amont

  • Annuler une plage : récupérez avec succès l'état de référence de la source, supprimez cette plage et envoyez l'instantané complet restant avec une révision plus récente.
  • Annuler toutes les plages : envoyez access_time_windows: [] avec une révision plus récente.
  • Laisser expirer une plage absolue : elle cesse d'autoriser l'accès à to ; envoyez ultérieurement un rapprochement sans l'entrée expirée pour garder des instantanés propres.
  • Mettre fin à une plage hebdomadaire : supprimez-la dans un instantané plus récent, ou utilisez active_until lorsque la source connaît l'instant de fin.
  • La récupération depuis la source échoue ou est incomplète : n'envoyez pas [] et ne publiez pas d'instantané partiel. Déclenchez une alerte et relancez la récupération. Transformer un échec en amont en instantané vide revient à révoquer l'accès involontairement.
  • La requête Partner API échoue à la validation : corrigez les données et n'envoyez une révision plus récente que si l'instantané normalisé change. Relancez les échecs temporaires avec la révision d'origine.

La fraîcheur des sources est configurée pour chaque intégration en fonction de la fréquence de livraison et de rapprochement qu'elle garantit. Avant l'activation, convenez de l'ancienneté maximale des sources, de ce qui se passe lorsqu'elle est dépassée, des seuils d'alerte et de la possibilité pour les opérateurs d'accepter de ne s'appuyer que sur les bornes propres à chaque plage. Une plage provenant d'une source obsolète ne devient jamais une règle de refus ; les règles habituelles et les autorisations permanentes restent des solutions de repli.

Algorithme du producteur

Utilisez cette procédure indépendante du langage pour chaque mise à jour de membre :

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)

Liste de contrôle de certification

Effectuez ces vérifications dans une installation de staging en utilisant la correspondance scope_key définitive :

  • Une plage absolue sans fuseau horaire est acceptée et évaluée en UTC.
  • Une plage absolue avec un fuseau IANA personnalisé conserve les mêmes instants from/to.
  • Une plage hebdomadaire avec un fuseau IANA personnalisé suit l'heure locale lors du passage à l'heure d'été et du retour à l'heure d'hiver.
  • Un instantané mixte absolu/hebdomadaire remplace entièrement l'instantané précédent.
  • L'accès commence exactement à from et s'arrête exactement à to ; les fins de blocs hebdomadaires sont également exclusives.
  • La modification des heures sous le même external_id avec une révision plus récente prend effet.
  • L'omission de access_time_windows modifie les autres champs du membre sans modifier les plages.
  • La relecture de la même révision/charge utile est idempotente.
  • La même révision avec une charge utile différente renvoie un conflit et ne modifie pas l'instantané.
  • Une révision plus ancienne reçue dans le désordre est signalée comme obsolète et ne peut pas rétablir un état antérieur.
  • Un [] plus récent révoque les plages appartenant à la source ; un échec de récupération en amont n'envoie rien.
  • Un scope_key inconnu ou sans correspondance entraîne un refus par défaut et génère un avertissement visible par les opérateurs.
  • Les alertes d'ancienneté des sources et la politique configurée pour les sources obsolètes fonctionnent au seuil convenu.
  • Les chemins de webhook et de rapprochement complet convergent vers le même instantané et le même ordre de révisions.
  • La livraison du flux atteint Door & Gate Access dans le délai convenu.
  • Une plage active peut autoriser l'accès à la porte physique associée, mais ne peut pas contourner la désactivation d'un utilisateur, blockAllAccess, la maintenance ou le géorepérage.
  • Avant, en dehors et après une plage, les règles habituelles et les autorisations permanentes continuent d'autoriser les utilisateurs qui remplissent par ailleurs les conditions.

Vecteurs de test minimaux

[
  {
    "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."
  }
]

Soumettez également des vecteurs négatifs pour les millisecondes Unix, les chaînes numériques, les nombres à virgule flottante, les fuseaux IANA invalides, from >= to, les external_id en double, les blocs qui se chevauchent, les blocs de nuit, plus de huit blocs dans une journée, plus de 50 plages et les clés de portée sans correspondance.