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
arrayfacultatifL'instantané complet des Access Time Windows pour ce membre et cette source partenaire. Un instantané peut combiner des plages
absoluteetweekly. Omettez le champ pour conserver l'instantané stocké tel quel ; envoyez[]pour révoquer toutes les plages appartenant à cette source. -
access_time_windows_revision
integerobligatoire avec access_time_windowsUn 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_windowsest présent et ne doit pas dépasser9007199254740991.
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. fromdoit être inférieur àto. Lorsque les deux bornes hebdomadaires sont présentes,active_fromdoit ê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 :
fromettodésignent déjà des instants UTC exacts.timezonen'est qu'une métadonnée d'affichage et d'audit ; la modifier ne décale jamais aucun des deux instants. - Plages hebdomadaires :
timezonedéfinit l'heure locale et les règles d'heure d'été utilisées pour évalueraccess_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_frometactive_untilrestent 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ée | Résultat |
|---|---|
access_time_windows omis | L'instantané existant reste inchangé ; aucune révision n'est requise |
| Présent avec une révision plus récente | L'intégralité de l'instantané stocké pour ce membre/cette source est remplacée |
| Même révision et même charge utile normalisée | Relecture idempotente ; aucune modification de l'instantané |
| Même révision et charge utile normalisée différente | Conflit ; attribuez une nouvelle révision après rapprochement |
| Révision plus ancienne | L'instantané obsolète est ignoré ; il ne peut pas écraser un état plus récent |
access_time_windows: [] avec une révision plus récente | Ré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_untillorsque 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 à
fromet s'arrête exactement àto; les fins de blocs hebdomadaires sont également exclusives. - La modification des heures sous le même
external_idavec une révision plus récente prend effet. - L'omission de
access_time_windowsmodifie 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_keyinconnu 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.