Introduction
The Performance Hub Partner API enables external partner platforms to seamlessly integrate with Performance Hub's core modules. The API supports computer vision, analytics, member access logging, and facility insights.
Request Format
The API accepts JSON-encoded request bodies and returns JSON-encoded responses.
Rate Limiting
To ensure fair usage and system stability, Partner API accounts start with default limits:
- 5 requests per second per endpoint
- Recommended: 1000 items maximum per batch request for optimal performance
- 10MB maximum payload size
Rate limits are adjustable and can be configured on a case-by-case basis by your Performance Hub partner account manager. If your integration needs higher throughput for launch, migrations, reporting backfills, or high-volume facilities, raise this during onboarding or before the planned change window.
Base URL
https://partner-api.performancehub.co/partner/v1/
Authentication
Access to the Partner API requires authentication using API secret keys. Generate and manage your keys through the Partner Console.
API keys serve as your credentials for accessing partner resources. Each key has restricted access to specific organisation data and can be deactivated instantly when needed.
Handle your API keys with care—they provide full access to your account! Never expose these keys in public repositories, client-side applications, or anywhere they could be compromised.
Authenticate requests using HTTP Basic Auth by passing your API key as the username. Leave the password empty.
The API only accepts secure HTTPS connections. Plain HTTP requests are rejected, as are any requests missing proper authentication credentials.
Example Request
curl https://partner-api.performancehub.co/partner/v1/organisations \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
# The colon prevents curl from asking for a password.
Verify Secret Key
curl https://partner-api.performancehub.co/partner/v1/auth/ping \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
# Returns { "message": "You are successfully authorised as <PartnerName>" }
Going-Live Checklist
Use this checklist before enabling a partner integration for production facilities.
Partner Console setup
- Your partner profile is complete.
- Your logo URL, website, category, and description are accurate.
- Your data linking method is configured: Direct Link or Linking Identifier.
- Facility linking has been tested with at least one real facility.
- The right key type is being used:
- Dryrun for non-destructive write testing.
- Read-only for safe data consumption.
- Live only when you are ready to affect production data.
Authentication and key management
- Secret keys are stored server-side only.
- Keys are not embedded in client-side applications, mobile apps, public repositories, or support tickets.
- Key rotation and revocation have been tested.
- Your team knows which key is used by each environment.
Data validation
- Required fields are present for every endpoint you call.
- Facility identifiers match your configured linking method.
- IDs from your system are stable and do not change between syncs.
- Large imports are split into batches under the documented limits.
- Failed batch items can be identified and resent.
Endpoint testing
Before launch, test every endpoint your integration depends on:
- Organisations and facilities can be read.
- Members can be saved and updated.
- Membership plans, access zones, and access hours sync correctly.
- Check-ins are accepted and can be correlated back to your system.
- Report snapshots use the expected reporting period and metrics.
- Door/Gate Controller flows are tested on real hardware where applicable.
Access Time Window readiness
If the integration supplies Access Time Windows:
- Absolute and weekly examples pass the producer certification checklist.
- Epochs are unquoted integer Unix seconds; exact
[from, to)boundaries have been tested. - UTC defaults, the integration's custom IANA timezones, and relevant daylight-saving transitions have been verified.
- One durable, monotonic member/source revision stream coordinates incremental updates, reconciliation, and retries.
- Operators understand omitted, complete-replacement, idempotent, conflicting, stale, and explicit-
[]behavior. - Every
scope_key, includingdefault, has an approved Performance Hub mapping; unknown keys have been tested to fail closed. - The integration's expected refresh cadence, maximum source age, stale-source behavior, and alerts are configured.
- An upstream fetch failure has been proven not to emit an empty or partial snapshot.
- Stage stream convergence, cancellation/revocation, and physical-door start/end boundaries meet the agreed targets.
- Regular rules and permanent grants have been verified as fallback paths, and windows cannot bypass user disablement,
blockAllAccess, maintenance, or geofencing.
Access control readiness
For access control integrations:
- Your system makes the access decision before calling Performance Hub.
- The integration handles
device_offline,maintenance_mode_active, andunlock_confirmation_timeout. - Operators understand that a timeout means the unlock was not confirmed.
- Your logs store the returned
event_idandcommand_id. - You have a manual fallback process for facility staff.
Error handling and retries
400,401,403,404,409, and422responses are surfaced to operators or support teams rather than retried blindly.429,500,502,503, and504responses use bounded retry logic with backoff.- Repeated failures create an alert in your system.
- The integration never drops failed records silently.
Observability
- Your logs include endpoint, timestamp, facility identifier, source record ID, request outcome, and response body.
- Your support team can search by member ID, facility ID, check-in ID, or unlock event ID.
- You have a way to replay or reconcile failed syncs.
Production launch
- Performance Hub has confirmed your production access.
- At least one production facility has enabled your integration.
- Your production key is active.
- Your operational and technical contacts are current.
- Your support process is documented for customers.
After these checks are complete, your configured integration will appear in the Performance Hub partner directory inside the core Performance Hub platform.
Best Practices
Use these patterns to keep partner integrations predictable, observable, and safe for facilities.
Identify facilities consistently
Choose the linking method configured for your partner account and use it consistently:
- Direct Link - use the Performance Hub
facility_idreturned by the facilities endpoint. - Linking Identifier - use your own location identifier, such as a branch ID, location code, or store number.
Do not send both identifiers in the same request. Performance Hub resolves requests using the linking method configured for your partner account.
See Partner Linking Methods for setup guidance.
Batch writes carefully
The Partner API accepts JSON request bodies and is designed for batch-oriented sync workflows where documented. Keep batches bounded and retryable.
Default guidance:
- Use up to 1000 items per batch request for optimal performance.
- Keep payloads under 10MB.
- Stay within 5 requests per second per endpoint.
Rate limits are adjustable and can be configured on a case-by-case basis by your Performance Hub partner account manager. If you expect high-volume imports, launch traffic, or reporting backfills, agree the required throughput before the change window.
If you need to sync a large dataset, split it into deterministic chunks and log each chunk ID on your side. This makes failed retries easier to reason about.
Retry safely
Use exponential backoff for transient failures such as 429, 500, 502, 503, and 504.
Avoid tight retry loops. A good starting point is:
- Retry after 1 second.
- Retry after 2 seconds.
- Retry after 4 seconds.
- Stop and alert if the operation still fails.
For validation failures such as 400 or 422, fix the request before retrying.
Use idempotency where supported
The Door/Gate Controller unlock endpoint supports an optional idempotency_key. Reusing the same key for the same partner, facility, and door returns the existing result when one is available.
Use a stable key for the action being performed, for example a check-in ID or access event ID from your system.
Do not assume every write endpoint supports idempotency unless it is documented for that endpoint.
Keep source data clean
Send stable, normalized values wherever possible:
- Use consistent member, plan, facility, and check-in IDs from your source system.
- Use ISO 8601 timestamps for dates and times.
- Send phone numbers in international format where available.
- Send currencies using the fields documented for each endpoint.
- Avoid overloading free-text fields with data that belongs in structured fields.
Design for incremental and full syncs
Most partner integrations need both:
- Incremental syncs for day-to-day changes.
- Full reconciliation syncs to correct missed events, historical imports, or data drift.
Your integration should be able to resend the current state for a member, membership plan, or report snapshot without depending on a previous request having succeeded.
Publish Access Time Windows safely
Treat each member's access_time_windows value as a complete, independently revisioned snapshot:
- Coordinate one monotonically increasing
access_time_windows_revisionstream across incremental and reconciliation workers. - Retry an uncertain or transient delivery with the same revision and normalized payload.
- Omit
access_time_windowswhen you are not updating it. - Send
[]only after a successful authoritative source fetch proves that no windows remain. Never turn an upstream timeout, partial result, or parse failure into revocation. - Keep
external_idandscope_keystable, and certify every scope mapping before production. - Monitor source age against the freshness policy agreed for that integration.
See the authoritative Access Time Windows producer guide for complete snapshots, timezones, DST, cancellation, and test vectors.
Treat access control as safety-critical
For Door/Gate Controller unlocks:
- Your system must decide whether the person is allowed to enter before calling Performance Hub.
- Performance Hub returns
200only after the on-site controller confirms the relay physically unlocked. - Most confirmations complete in 3-5 seconds.
- A timeout is an explicit failed unlock, not a background success.
- Retry only according to your own access and safety rules.
Always log the event_id and command_id returned by the unlock endpoint.
Monitor your integration
Use the Partner Console request logs during development and launch. In production, also maintain your own logs with:
- Request timestamp.
- Endpoint.
- Source record ID.
- Facility identifier.
- HTTP status code.
- Performance Hub response body or error message.
These fields make support and reconciliation much faster when a customer asks what happened.
Organisations
Represents the top-level business entity that operates one or more facilities integrated with the Performance Hub platform.
Endpoints
GET /partner/v1/organisations - List all organisations
The Organisation object
Attributes
org_id
stringrequiredUnique identifier for the organisation.
name
stringrequiredOrganisation name.
The Organisation object
{
"org_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"name": "FitLife Fitness Group"
}
List all organisations
Returns a list of organisations accessible to your partner account.
Returns
Returns a JSON object with a data property containing an array of Organisation objects.
Request
curl https://partner-api.performancehub.co/partner/v1/organisations \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
Response
{
"data": [
{
"org_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"name": "FitLife Fitness Group"
},
{
"org_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"name": "Wellness Studios Network"
}
]
}
Facilities
Represents a physical location, branch, club, gym, or studio where members can access services.
Endpoints
GET /partner/v1/facilities - List all facilities
The Facility object
Attributes
facility_id
stringrequiredUnique identifier for the facility.
org_id
stringrequiredUnique identifier for the parent organisation.
name
stringrequiredFacility name.
brand
stringoptionalBrand name used by the facility.
website_url
stringoptionalFacility website URL.
place_data
objectoptionalLocation data from Google Places API containing country, formatted address, Google Place ID, province, and city.
social_links
objectoptionalSocial media links for the facility.
timezone
stringoptionalIANA timezone identifier (e.g.,
"Australia/Sydney","America/New_York").
The Facility object
{
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"org_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"name": "FitLife Downtown",
"brand": "FitLife",
"website_url": "https://fitlife.com/downtown",
"social_links": {
"instagram": {
"handle": "@fitlifedowntown"
},
"facebook": {
"handle": "FitLifeDowntown"
}
},
"place_data": {
"country": {
"iso_code": "AU",
"full_name": "Australia"
},
"formatted_address": "123 Main Street, Suite 100, Sydney NSW 2000, Australia",
"google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"province": {
"abbreviation": "NSW",
"full_name": "New South Wales"
},
"city": {
"short_name": "Sydney",
"full_name": "Sydney"
}
},
"timezone": "Australia/Sydney"
}
List all facilities
Returns a list of facilities accessible to your partner account.
Returns
Returns a JSON object with a data property containing an array of Facility objects.
Request
curl https://partner-api.performancehub.co/partner/v1/facilities \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
Response
{
"data": [
{
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"org_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"name": "FitLife Downtown",
"brand": "FitLife",
"website_url": "https://fitlife.com/downtown",
"place_data": {
"country": {
"iso_code": "AU",
"full_name": "Australia"
},
"formatted_address": "123 Main Street, Sydney NSW 2000, Australia",
"google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"province": {
"abbreviation": "NSW",
"full_name": "New South Wales"
},
"city": {
"short_name": "Sydney",
"full_name": "Sydney"
}
},
"timezone": "Australia/Sydney"
}
]
}
Membership Plans
Defines a purchasable offering from an organisation or facility, including services, entitlements, schedule, and pricing.
Endpoints
POST /partner/v1/membership-plans - Save a membership plan
The Membership Plan object
Attributes
membership_plan_id
stringrequiredUnique identifier for the membership plan.
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier of the facility from Performance Hub.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier value for each location that the partner uses (e.g., "Branch ID", "Location Code", "Store Number"). Learn more
name
stringrequiredHuman-readable plan name (e.g.,
"Unlimited Monthly","10 Class Pack").billing_cycle
stringoptionalBilling frequency. One of
weekly,fortnightly,monthly,annually, orone_time.price
integeroptionalBase cost in smallest currency unit (cents for USD/AUD).
currency
stringoptionalISO 4217 currency code (e.g.,
"AUD","USD").description
stringoptionalOptional marketing or internal description.
active_members
integeroptionalNumber of active members using this membership plan
is_recurring
booleanoptionalWhether the plan auto-renews.
is_trial
booleanoptionalWhether this is a trial offering.
visible_to_public
booleanoptionalWhether the plan is available for self-service purchase.
duration_days
integeroptionalNumber of days the membership plan is valid for.
access_zones
arrayoptionalArray of access zones included in the membership plan. Each zone can define its own access hours. See Access Zones for details.
class_limit
integeroptionalMaximum number of classes allowed (null for unlimited).
The Membership Plan object
{
"membership_plan_id": "plan_99999",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"name": "Unlimited Monthly",
"description": "Unlimited access to all gym facilities and group classes",
"billing_cycle": "monthly",
"price": 8900,
"currency": "AUD",
"duration_days": 30,
"is_recurring": true,
"is_trial": false,
"visible_to_public": true,
"active_members": 245,
"access_zones": [
{
"name": "Main Gym Floor",
"description": "Full access to cardio and weight training equipment",
"access_hours": {
"mon": [["05:00", "22:00"]],
"tue": [["05:00", "22:00"]],
"wed": [["05:00", "22:00"]],
"thu": [["05:00", "22:00"]],
"fri": [["05:00", "22:00"]],
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
},
{
"name": "Group Fitness Studio",
"description": "Access to group fitness classes",
"access_hours": {
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
}
],
"class_limit": null
}
Access Zones
Access zones define specific areas within a facility where a membership plan grants access. Each zone can have its own access hours, allowing different time restrictions per area.
Structure
Each access zone includes:
name
stringrequiredThe name of the zone (e.g., "Main Gym Floor", "Group Fitness Studio").
description
stringoptionalDescription of what's included in that zone.
access_hours
objectoptionalAccess hours specific to this zone. See Access Hours for the time range format.
Example
"access_zones": [
{
"name": "Main Gym Floor",
"description": "Full access to cardio and weight training equipment",
"access_hours": {
"mon": [["05:00", "22:00"]],
"tue": [["05:00", "22:00"]],
"wed": [["05:00", "22:00"]],
"thu": [["05:00", "22:00"]],
"fri": [["05:00", "22:00"]],
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
},
{
"name": "Group Fitness Studio",
"description": "Access to group fitness classes",
"access_hours": {
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
}
]
Access Hours
Access hours specify when members can access a specific zone during each day of the week. Access hours are defined per access zone, allowing different time restrictions for different areas of a facility.
Structure
{
"mon": [["start_time", "end_time"]],
"tue": [["start_time", "end_time"]],
"wed": [["start_time", "end_time"]],
"thu": [["start_time", "end_time"]],
"fri": [["start_time", "end_time"]],
"sat": [["start_time", "end_time"]],
"sun": [["start_time", "end_time"]]
}
Each day is an array of time ranges in HH:MM format (24-hour clock). An empty array [] means no access on that day. Days that are omitted are treated the same as an empty array.
Example Use Cases
// Early Bird Access
// Morning-only gym access, weekdays only:
"access_hours": {
"mon": [["04:00", "12:00"]],
"tue": [["04:00", "12:00"]],
"wed": [["04:00", "12:00"]],
"thu": [["04:00", "12:00"]],
"fri": [["04:00", "12:00"]]
}
// Off-Peak Plan
// Avoids peak hours, weekdays only:
"access_hours": {
"mon": [["10:00", "12:00"], ["14:00", "16:00"]],
"tue": [["10:00", "12:00"], ["14:00", "16:00"]],
"wed": [["10:00", "12:00"], ["14:00", "16:00"]],
"thu": [["10:00", "12:00"], ["14:00", "16:00"]],
"fri": [["10:00", "12:00"], ["14:00", "16:00"]]
}
// Weekend Warrior
// Full-day access on weekends:
"access_hours": {
"sat": [["08:00", "20:00"]],
"sun": [["08:00", "20:00"]]
}
// 24/7 Access (Most common use case)
// Always-on access, unlimited:
"access_hours": {
"mon": [["00:00", "23:59"]],
"tue": [["00:00", "23:59"]],
"wed": [["00:00", "23:59"]],
"thu": [["00:00", "23:59"]],
"fri": [["00:00", "23:59"]],
"sat": [["00:00", "23:59"]],
"sun": [["00:00", "23:59"]]
}
Per-Zone Example
"access_zones": [
{
"name": "Main Gym Floor",
"description": "Full access to cardio and weight training equipment",
"access_hours": {
"mon": [["05:00", "22:00"]],
"tue": [["05:00", "22:00"]],
"wed": [["05:00", "22:00"]],
"thu": [["05:00", "22:00"]],
"fri": [["05:00", "22:00"]],
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
},
{
"name": "Swimming Pool",
"description": "Access to swimming pool and change rooms",
"access_hours": {
"mon": [["06:00", "20:00"]],
"tue": [["06:00", "20:00"]],
"wed": [["06:00", "20:00"]],
"thu": [["06:00", "20:00"]],
"fri": [["06:00", "20:00"]],
"sat": [["08:00", "18:00"]],
"sun": [["08:00", "18:00"]]
}
}
]
Save a membership plan
Creates or updates membership plan records. The API determines whether to create or update based on the membership_plan_id.
Parameters
membership_plans
arrayrequiredArray of Membership Plan objects to save. Recommended maximum: 1000 items per request for optimal performance. The API automatically batches items for processing. Each item must include either
facility_id(required if not usinglinking_identifier) orlinking_identifier(required if not usingfacility_id).
Returns
Returns a simple message confirming the sync operation.
Request
curl -X POST https://partner-api.performancehub.co/partner/v1/membership-plans \
-u sk_live_YOUR_PARTNER_SECRET_KEY: \
-H "Content-Type: application/json" \
-d '{
"membership_plans": [
{
"membership_plan_id": "plan_99999",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"name": "Unlimited Monthly",
"description": "Unlimited access to all gym facilities and group classes",
"billing_cycle": "monthly",
"price": 8900,
"currency": "AUD",
"duration_days": 30,
"is_recurring": true,
"is_trial": false,
"visible_to_public": true,
"active_members": 245,
"access_zones": [
{
"name": "Main Gym Floor",
"description": "Full access to cardio and weight training equipment",
"access_hours": {
"mon": [["05:00", "22:00"]],
"tue": [["05:00", "22:00"]],
"wed": [["05:00", "22:00"]],
"thu": [["05:00", "22:00"]],
"fri": [["05:00", "22:00"]],
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
},
{
"name": "Group Fitness Studio",
"description": "Access to group fitness classes",
"access_hours": {
"sat": [["06:00", "20:00"]],
"sun": [["07:00", "18:00"]]
}
}
],
"class_limit": null
}
]
}'
Response
Returns a success message with metadata about the sync operation.
{
"message": "Membership plan sync successful.",
"metadata": {
"success": 98,
"skipped": 2,
"skipped_items": [
{
"membership_plan_id": "plan_456",
"facility_id": null,
"reason": "Missing facility_id"
}
]
}
}
Metadata fields:
- success - Number of membership plans successfully processed
- skipped - Number of plans skipped (if any)
- skipped_items - Array of skipped plans with reasons (if any)
- dry-run - Boolean flag if using dryrun key type (if applicable)
Members
Represents an individual customer of a facility. Can be a lead, prospect, recurring customer, expired member, or blocked user.
Member upserts can also carry a complete, revisioned access_time_windows snapshot containing absolute and recurring weekly access. See Access Time Windows before producing these fields. They are additive permissions and do not replace the facility's regular rules or permanent grants.
Endpoints
POST /partner/v1/members - Save a member
The Member object
Attributes
member_id
stringrequiredUnique identifier for the member.
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier of the facility from Performance Hub.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier value for each location that the partner uses (e.g., "Branch ID", "Location Code", "Store Number"). Learn more
first_name
stringrequiredMember's first name.
last_name
stringrequiredMember's last name.
email
stringoptionalEmail address.
mobile_phone
stringoptionalMobile phone number in E.123 format (e.g.,
"+1 415 555 2671").address
objectoptionalPhysical address object with the following properties:
address_line_1(string) - Primary street addressaddress_line_2(string, optional) - Secondary address linecity(string) - City namestate(string) - State/province codepostal_code(string) - Postal/ZIP codecountry(string) - ISO 3166-1 alpha-2 country code (e.g., "AU", "US")country_name(string, optional) - Full country name
date_of_birth
stringoptionalBirth date in
YYYY-MM-DDformat.gender
stringoptionalOne of
male,female,non_binary,prefer_not_to_say, orunknown.type
stringoptionalMember type (e.g.
staff,member,prospect).tags
arrayoptionalQuick helper strings or texts to categorise and identify specific members (e.g.
["vip", "personal-training", "student"]).membership_status
stringoptionalCurrent membership status (e.g.,
active,expired,on_hold).membership_plans
arrayoptionalArray of membership plans associated with the member.
profile_picture
stringoptionalBase64-encoded profile picture (optional).
created
stringoptionalISO 8601 timestamp when the member was created.
updated
stringoptionalISO 8601 timestamp when the member was last updated.
access_time_windows
arrayoptionalComplete source-owned snapshot of absolute and/or recurring weekly Access Time Windows. Omit the field to leave the snapshot unchanged; send an empty array with a newer revision to revoke it. See Access Time Windows for the shapes, validation, scope mapping, and replacement semantics.
access_time_windows_revision
integerrequired with access_time_windowsMonotonically increasing, non-negative JSON-safe integer for this member/source stream. Required whenever
access_time_windowsis present.
The Member object
{
"member_id": "member_11111",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@email.com",
"mobile_phone": "+61 412 345 678",
"date_of_birth": "1985-06-15",
"gender": "male",
"type": "member",
"tags": ["vip", "personal-training"],
"profile_picture": "/9j/4AAQSkZJRgABAQAAAQ...",
"membership_status": "current",
"created": "2024-01-15T10:30:00Z",
"updated": "2024-01-20T14:45:00Z",
"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"
}
],
"address": {
"address_line_1": "456 Bourke Street",
"address_line_2": "Apt 12",
"city": "Melbourne",
"state": "VIC",
"postal_code": "3000",
"country": "AU",
"country_name": "Australia"
},
"membership_plans": [
{
"membership_plan_id": "plan_99999",
"name": "Unlimited Monthly",
"start_date": "2024-01-01T00:00:00Z",
"status": "current",
"is_current": true,
"end_date": null
}
]
}
Access Time Windows
Access Time Windows let a Partner Member carry temporary or recurring access supplied by an external system. This page is the authoritative producer guide for access_time_windows and access_time_windows_revision.
Access Time Windows are additive allow-only permissions. An active window can allow access, but an inactive, expired, stale, or unmapped window does not deny access that a regular rule or permanent grant already allows. User disablement, explicit door blocking, maintenance mode, geofencing, rate limits, and controller availability still apply.
Implementations for particular upstream systems are separate adapters. They must translate their source data into this vendor-neutral contract without changing its field meanings.
Member snapshot fields
access_time_windows
arrayoptionalThe complete Access Time Window snapshot for this member and partner source. A snapshot can mix
absoluteandweeklywindows. Omit the field to leave the stored snapshot unchanged; send[]to revoke every window owned by this source.access_time_windows_revision
integerrequired with access_time_windowsA monotonically increasing, non-negative JSON-safe integer for this member/source revision stream. It is required whenever
access_time_windowsis present and must not exceed9007199254740991.
Each snapshot accepts at most 50 windows. external_id, scope_key, and label accept at most 120 characters. Absolute windows can span at most 90 days. A weekly day accepts at most eight non-overlapping blocks. Invalid data is rejected rather than truncated or guessed.
Window shapes
An absolute window identifies one exact interval:
Complete mixed request
This request replaces the member's previous source-owned snapshot with one absolute and one weekly window:
Time and boundary rules
from, to, active_from, and active_until are JSON integer Unix seconds identifying UTC instants:
- Send an unquoted JSON integer. Numeric strings, floats, RFC 3339 strings, and Unix milliseconds are rejected.
- Every interval is half-open:
[from, to). Access can start exactly atfrom; it is no longer active exactly atto. frommust be less thanto. When both weekly bounds are present,active_frommust be less thanactive_until.- Do not derive epochs from an unzoned local date/time. First resolve the source time to a UTC instant, then divide Unix milliseconds by 1000 and verify the result is an integer.
Timezones and daylight saving
timezone must be a valid IANA timezone such as UTC, Australia/Perth, or America/New_York. It defaults to UTC when omitted.
- Absolute windows:
fromandtoalready identify exact UTC instants.timezoneis display and audit metadata only; changing it never shifts either instant. - Weekly windows:
timezonedefines the local wall clock and daylight-saving rules used to evaluateaccess_hours. Do not pre-convert weekly blocks to UTC. - During a spring-forward change, a local time that the timezone skips never occurs.
- During a fall-back change, both real instants carrying the repeated local time match the weekly block.
active_fromandactive_untilremain UTC instants even when a weekly window uses a non-UTC timezone.
access_hours uses lowercase sun through sat. Each value is an array of [start, end] pairs using zero-padded, 24-hour HH:mm. Blocks must be non-empty, ordered, and non-overlapping. end is exclusive. A block cannot cross midnight; split an overnight source interval into a late block on the first day and an early block on the next day.
Stable IDs and scope mapping
external_id must be stable for the lifetime of a logical window and unique within the member snapshot. Keep it stable when times or labels change so operators and audit records can correlate revisions. Do not reuse it for an unrelated window.
scope_key is your stable logical access target. It defaults to default. A facility administrator maps each allowed key to Performance Hub facility-wide, door, or Access Scope targets:
Complete snapshots and revisions
Treat the two fields as a separately versioned partition within the Member upsert:
| Input | Result |
|---|---|
access_time_windows omitted |
Existing snapshot remains unchanged; no revision is required |
| Present with a newer revision | Entire stored snapshot for this member/source is replaced |
| Same revision and same normalized payload | Idempotent replay; no snapshot change |
| Same revision and different normalized payload | Conflict; allocate a new revision after reconciling |
| Older revision | Stale snapshot is skipped; it cannot overwrite newer state |
access_time_windows: [] with a newer revision |
Explicitly revokes all source-owned windows |
A snapshot is complete, not a patch. If revision 1842 has windows A and B, then revision 1843 containing only B removes A.
Maintain one coordinated revision stream per partner/member across webhook workers, polling, replay, and full reconciliation. A durable database sequence is preferred. A Unix-millisecond value can be used as the revision only when every writer coordinates and guarantees strict ordering and uniqueness; it is a revision value, not an epoch field.
Persist the exact request and revision until the API response is known. Retry the same normalized snapshot with the same revision after a timeout or transient error. Do not allocate a new revision merely because transport delivery is uncertain.
Cancellation, expiry, and upstream failures
- Cancel one window: successfully fetch authoritative source state, remove that window, and send the remaining complete snapshot with a newer revision.
- Cancel all windows: send
access_time_windows: []with a newer revision. - Let an absolute window expire: it stops allowing at
to; send a later reconciliation without the expired entry to keep snapshots clean. - End a weekly window: remove it in a newer snapshot, or use
active_untilwhen the source has a known end instant. - Source fetch fails or is incomplete: do not send
[]and do not publish a partial snapshot. Alert and retry the fetch. Turning an upstream failure into an empty snapshot is an unintended access revocation. - Partner API request fails validation: correct the data and send a newer revision only if the normalized snapshot changes. Retry transient failures with the original revision.
Source freshness is configured per integration from its promised delivery and reconciliation cadence. Before activation, agree the maximum source age, what happens when it is exceeded, alert thresholds, and whether operators may acknowledge relying only on each window's own bounds. A stale source window never becomes a deny rule; regular rules and permanent grants remain fallback paths.
Producer algorithm
Use this language-neutral workflow for every member update:
Certification checklist
Complete these checks in a stage facility using the final scope_key mapping:
- An absolute window is accepted with omitted timezone and is evaluated as UTC.
- A custom-IANA absolute window preserves the same
from/toinstants. - A weekly custom-IANA window follows local wall time across spring-forward and fall-back transitions.
- A mixed absolute/weekly snapshot replaces the prior snapshot completely.
- Access starts exactly at
fromand stops exactly atto; weekly block ends are also exclusive. - Updating times under the same
external_idwith a newer revision takes effect. - Omitting
access_time_windowschanges other Member fields without changing windows. - Replaying the same revision/payload is idempotent.
- Same revision/different payload returns a conflict and does not change the snapshot.
- An older out-of-order revision is reported stale and cannot roll state back.
- A newer
[]revokes source-owned windows; an upstream fetch failure sends nothing. - An unknown or unmapped
scope_keyfails closed and creates an operator-visible warning. - Source-age alerts and the configured stale-source policy work at the agreed threshold.
- Webhook and full reconciliation paths converge on the same snapshot and revision order.
- Stream delivery reaches Door & Gate Access within the agreed target.
- An active window can allow the mapped physical door, but cannot bypass user disablement,
blockAllAccess, maintenance, or geofencing. - Before, outside, and after a window, regular rules and permanent grants still allow users who otherwise qualify.
Related guidance
{
"external_id": "access-4b1782",
"kind": "absolute",
"from": 1792053000,
"to": 1792058400,
"timezone": "Australia/Perth",
"scope_key": "after_hours_entry",
"label": "Temporary site access"
}
Required fields are external_id, kind, from, and to. timezone, scope_key, and label are optional.
A weekly window identifies recurring local wall-clock intervals, optionally bounded by UTC instants:
{
"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"
}
Required fields are external_id, kind, and access_hours. timezone, active_from, active_until, scope_key, and label are optional.
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 either facility_id or linking_identifier according to your configured linking method. Do not send both.
Epoch conversion
2026-10-15T08:30:00Z converts to 1792053000:
UTC instant Unix milliseconds Unix seconds sent
2026-10-15T08:30:00Z 1792053000000 1792053000
Language-neutral conversion:
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
If the source has sub-second precision, define a deliberate start/end rounding policy before integration testing. Never pass milliseconds and rely on the API to infer the unit.
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
Partners do not send Performance Hub facility, door, or Access Scope IDs inside a window. An unknown, disabled, or unmapped scope_key fails closed for that window; it does not broaden access. Agree and test mappings before launch.
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)
Minimum test vectors
[
{
"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."
}
]
Also submit negative vectors for Unix milliseconds, numeric strings, floats, invalid IANA zones, from >= to, duplicate external_id, overlapping blocks, overnight blocks, more than eight blocks in a day, more than 50 windows, and unmapped scope keys.
Save a member
Creates or updates member records. The API determines whether to create or update based on the member_id.
Parameters
members
arrayrequiredArray of Member objects to save. Recommended maximum: 1000 items per request for optimal performance. The API automatically batches items for processing. Each item must include either
facility_id(required if not usinglinking_identifier) orlinking_identifier(required if not usingfacility_id).When a member includes
access_time_windows, it must also includeaccess_time_windows_revision. The array is a complete source snapshot, not a patch. Omit it to preserve the existing snapshot, or send[]with a newer revision to revoke all source-owned windows. Read the Access Time Windows guide before enabling this field.
Returns
Returns a simple message confirming the sync operation.
Member profile fields remain normal upserts even when a window snapshot is reported as an idempotent replay or stale skip. A conflicting same-revision/different-payload snapshot is not retryable unchanged; reconcile source state and send a newer revision.
Request
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",
// "linking_identifier": "branch_downtown", // If using linking identifier
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@email.com",
"mobile_phone": "+61 412 345 678",
"date_of_birth": "1985-06-15",
"gender": "male",
"type": "member",
"tags": ["vip", "personal-training"],
"profile_picture": "/9j/4AAQSkZJRgABAQAAAQ...",
"membership_status": "current",
"created": "2024-01-15T10:30:00Z",
"updated": "2024-01-20T14:45:00Z",
"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"
}
],
"address": {
"address_line_1": "456 Bourke Street",
"address_line_2": "Apt 12",
"city": "Melbourne",
"state": "VIC",
"postal_code": "3000",
"country": "AU",
"country_name": "Australia"
},
"membership_plans": [
{
"membership_plan_id": "plan_99999",
"name": "Unlimited Monthly",
"start_date": "2024-01-01T00:00:00Z",
"status": "current",
"is_current": true,
"end_date": null
}
]
}
]
}'
Response
Returns a success message with metadata about the sync operation.
{
"message": "Member sync successful.",
"metadata": {
"success": 245,
"skipped": 5,
"skipped_items": [
{
"member_id": "member_123",
"facility_id": null,
"reason": "Facility integration is disabled in facility-partner-config"
}
]
}
}
Metadata fields:
- success - Number of members successfully processed
- skipped - Number of members skipped (if any)
- skipped_items - Array of skipped members with reasons (if any)
- dry-run - Boolean flag if using dryrun key type (if applicable)
Check-Ins
Represents a record of a person being present at a facility, class, or activity. Used for tracking attendance, usage analytics, and improving visual detection accuracy.
Endpoints
POST /partner/v1/check-ins - Save check-ins
The Check-In object
Attributes
check_in_id
stringoptionalUnique identifier for the check-in. Optional - auto-generated from
member_idandtimestamppair if not provided.member_id
stringrequiredUnique identifier of the member.
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier of the facility from Performance Hub.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier value for each location that the partner uses (e.g., "Branch ID", "Location Code", "Store Number"). Learn more
check_in_type
stringoptionalType of visit. One of
member,class,staff,guest, ortrial.source
stringoptionalHow the check-in was captured. Common values include
manual,tag,rfid,kiosk,app. Additional values likedoor_readerorbookingmay also be used.class_id
stringoptionalOptional. If tied to a class booking or attendance.
timestamp
stringrequiredISO 8601 format timestamp of when the check-in occurred.
The Check-In object
{
// "check_in_id": "checkin_abc123", // Optional - auto-generated from member_id + timestamp if not provided
"member_id": "mem_12345",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"check_in_type": "member",
"source": "rfid",
"timestamp": "2025-01-20T08:45:30Z"
}
Save check-ins
Creates check-in records. The API automatically deduplicates based on member, facility, and timestamp.
Parameters
check_ins
arrayrequiredArray of Check-In objects to save. Recommended maximum: 1000 items per request for optimal performance. The API automatically batches items for processing. Each item must include either
facility_id(required if not usinglinking_identifier) orlinking_identifier(required if not usingfacility_id).
Returns
Returns a simple message confirming the sync operation.
Request
curl -X POST https://partner-api.performancehub.co/partner/v1/check-ins \
-u sk_live_YOUR_PARTNER_SECRET_KEY: \
-H "Content-Type: application/json" \
-d '{
"check_ins": [
{
// "check_in_id": "checkin_abc123", // Optional - auto-generated from member_id + timestamp if not provided
"member_id": "mem_12345",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"check_in_type": "member",
"source": "rfid",
"timestamp": "2025-01-20T08:45:30Z"
}
]
}'
Response
Returns a success message with metadata about the sync operation.
{
"message": "Check-in sync successful.",
"metadata": {
"success": 523,
"skipped": 3,
"skipped_items": [
{
"member_id": "member_789",
"facility_id": null,
"reason": "No matching facility_id for the linking_identifier"
}
]
}
}
Metadata fields:
- success - Number of check-ins successfully processed
- skipped - Number of check-ins skipped (if any)
- skipped_items - Array of skipped check-ins with reasons (if any)
- dry-run - Boolean flag if using dryrun key type (if applicable)
Access Control
The Partner API can trigger Performance Hub Door/Gate Controller hardware for facilities where your integration is enabled.
Use this API when your member management system has already decided that a person is allowed to enter. Performance Hub does not evaluate membership status, access hours, door-zone rules, fraud checks, or suspension logic. Your system remains the authority for those decisions.
The unlock request is synchronous. Performance Hub sends the command to the on-site controller over MQTT and returns 200 only after the device confirms that the relay physically unlocked. Most confirmations should complete in 3-5 seconds. If no confirmation is received within the timeout window, the API returns an explicit failure.
Endpoints
GET /partner/v1/access-control/doors - List available Door/Gate Controllers
GET /partner/v1/access-control/doors/{door_id} - Get Door/Gate Controller status
POST /partner/v1/access-control/doors/{door_id}/unlock - Unlock a Door/Gate Controller
The Door/Gate Controller object
Attributes
door_id
stringrequiredUnique identifier for the Door/Gate Controller. Store this in your system after discovery and use it when calling the unlock endpoint.
facility_id
stringrequiredPerformance Hub facility ID.
linking_identifier
stringoptionalPartner facility identifier when your integration uses linking identifiers. Learn more
name
stringrequiredHuman-readable device name configured in Performance Hub.
zone
stringoptionalDoor zone or location, such as
Entrance.status
stringrequiredonlineoroffline.relay_state
stringoptionalLatest known relay state, such as
lockedorunlocked.maintenance_mode
stringrequiredCurrent maintenance mode. One of
off,force-open, orforce-closed.hold_time_seconds
integerrequiredDefault unlock hold time configured for the door.
last_seen
stringoptionalISO timestamp for the latest device metadata update.
The Door/Gate Controller object
{
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"linking_identifier": "branch_downtown",
"name": "Front Door",
"zone": "Entrance",
"status": "online",
"online": true,
"relay_state": "locked",
"maintenance_mode": "off",
"hold_time_seconds": 10,
"last_seen": "2026-05-01T04:30:00.000Z"
}
List Door/Gate Controllers
Returns Door/Gate Controllers available to your integration.
If neither facility_id nor linking_identifier is provided, the API returns Door/Gate Controllers across all facilities linked to your partner account.
Parameters
facility_id
stringoptional either/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn morePerformance Hub facility ID. Use this for direct-link integrations.
linking_identifier
stringoptional either/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreYour own facility/location identifier. Use this when your integration is configured for linking identifiers.
Returns
Returns an array of Door/Gate Controller objects.
Request
curl -X GET "https://partner-api.performancehub.co/partner/v1/access-control/doors?facility_id=550e8400-e29b-41d4-a716-446655440000" \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
Response
{
"data": [
{
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Front Door",
"zone": "Entrance",
"status": "online",
"relay_state": "locked",
"maintenance_mode": "off",
"hold_time_seconds": 10,
"last_seen": "2026-05-01T04:30:00.000Z"
}
]
}
Get Door/Gate Controller status
Retrieves the latest known state and basic configuration for a single Door/Gate Controller.
Provide a facility identifier so Performance Hub can verify that the door belongs to a facility your integration can access.
Parameters
door_id
stringrequiredDoor/Gate Controller ID returned by the list endpoint.
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn morePerformance Hub facility ID. Required for direct-link integrations.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn morePartner facility identifier. Required for linking-identifier integrations.
Returns
Returns a Door/Gate Controller object.
Request
curl -X GET "https://partner-api.performancehub.co/partner/v1/access-control/doors/577393ce6ec584aa4e2e2b1d1e73934c?facility_id=550e8400-e29b-41d4-a716-446655440000" \
-u sk_live_YOUR_PARTNER_SECRET_KEY:
Response
{
"data": {
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Front Door",
"zone": "Entrance",
"status": "online",
"relay_state": "locked",
"maintenance_mode": "off",
"hold_time_seconds": 10,
"last_seen": "2026-05-01T04:30:00.000Z"
}
}
Unlock a Door/Gate Controller
Sends an unlock command to the Door/Gate Controller and waits for the physical relay to confirm it has unlocked.
Your system must decide whether the person is authorised before calling this endpoint. Performance Hub only verifies the partner key, facility access, door ownership, and device state.
Parameters
door_id
stringrequiredDoor/Gate Controller ID returned by the list endpoint.
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn morePerformance Hub facility ID. Required for direct-link integrations.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn morePartner facility identifier. Required for linking-identifier integrations.
member_id
stringrequiredPartner member ID. This is used for audit logging and later data linking.
first_name
stringrequiredFirst name of the person opening the door.
last_name
stringrequiredLast name of the person opening the door.
full_name
stringoptionalOptional. If omitted, Performance Hub combines
first_nameandlast_name.check_in_id
stringoptionalOptional check-in ID from your system for correlation.
idempotency_key
stringoptionalOptional replay-protection key. Reusing the same key for the same partner, facility, and door returns the existing result when available.
Returns
Returns 200 only when the device confirms the relay physically unlocked. The door uses the hold time configured in Performance Hub Device Management; partners cannot override relay timing per request. Most confirmations should complete in 3-5 seconds. Treat the documented timeout as an explicit failed unlock and retry only according to your own safety rules.
Common failures:
403 integration_not_enabled- your integration is not enabled for the facility.409 maintenance_mode_active- facility staff have enabled maintenance mode for the door.503 device_offline- the Door/Gate Controller is offline or stale.504 unlock_confirmation_timeout- the command was sent but no matching confirmation was received before timeout.
Request
curl -X POST https://partner-api.performancehub.co/partner/v1/access-control/doors/577393ce6ec584aa4e2e2b1d1e73934c/unlock \
-u sk_live_YOUR_PARTNER_SECRET_KEY: \
-H "Content-Type: application/json" \
-d '{
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"member_id": "mem_12345",
"first_name": "Jane",
"last_name": "Smith",
"full_name": "Jane Smith",
"source": "door_reader",
"idempotency_key": "unlock_20260501_0001"
}'
Response
{
"status": "unlocked",
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"event_id": "2d98e9ff-3a2a-46d4-a22f-5f23f7d75b11",
"command_id": "9d4d013d-d929-4a87-8b52-6cc66f56ff07",
"hold_time_seconds": 10,
"confirmed_at": "2026-05-01T04:30:00.000Z"
}
Failure response
{
"status": "failed",
"reason": "device_offline",
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"event_id": "2d98e9ff-3a2a-46d4-a22f-5f23f7d75b11"
}
Facility Reports
A point-in-time record capturing key operational and financial metrics for a facility during a specific period.
Endpoints
POST /partner/v1/report-snapshots - Save report snapshots
The Report Snapshot object
Attributes
facility_id
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier of the facility from Performance Hub.
linking_identifier
stringeither/orProvide the facility identifier type configured for your integration: use facility_id for direct-link integrations, or linking_identifier for linking-identifier integrations. Do not send both; the API uses the identifier type configured for your partner account and ignores the other one. Learn moreUnique identifier value for each location that the partner uses (e.g., "Branch ID", "Location Code", "Store Number"). Learn more
start_date
stringrequiredSnapshot period start date in
YYYY-MM-DDformat.end_date
stringrequiredSnapshot period end date in
YYYY-MM-DDformat.metrics
objectrequiredMetrics object containing member and revenue data for the period.
The Report Snapshot object
{
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"start_date": "2025-01-01",
"end_date": "2025-01-31",
"metrics": {
"members": {
"active": 1200,
"new": 45,
"cancelled": 12
},
"revenue": {
"total_payments": 289000.00,
"total_payments_count": 1200,
"currency": "AUD"
}
}
}
Save report snapshots
Submits operational and financial metrics for a facility during a specific period.
Parameters
snapshots
arrayrequiredArray of Report Snapshot objects to save. Maximum 25 items per request. Each item must include either
facility_id(required if not usinglinking_identifier) orlinking_identifier(required if not usingfacility_id).
Returns
Returns a simple message confirming the sync operation.
Request
curl -X POST https://partner-api.performancehub.co/partner/v1/report-snapshots \
-u sk_live_YOUR_PARTNER_SECRET_KEY: \
-H "Content-Type: application/json" \
-d '{
"snapshots": [
{
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
// "linking_identifier": "branch_downtown", // If using linking identifier
"start_date": "2025-01-01",
"end_date": "2025-01-31",
"metrics": {
"members": {
"active": 1200,
"new": 45,
"cancelled": 12
},
"revenue": {
"total_payments": 289000.00,
"currency": "AUD"
}
}
}
]
}'
Response
{
"message": "Report snapshot sync successful."
}
Errors
The Partner API uses standard HTTP status codes and JSON response bodies to explain why a request failed.
Error response shape
Most API errors return a JSON body with a message and, where available, structured details.
Status codes
Access Time Window failures
Window validation fails before the invalid snapshot is written. Common non-retryable causes include:
- Missing
access_time_windows_revisionwhenaccess_time_windowsis present. - String, floating-point, or likely-millisecond epoch values.
- An invalid IANA timezone, inverted interval, duplicate
external_id, overlapping weekly blocks, or an overnight block that was not split. - A value outside documented count, length, or span limits.
A repeated revision with different normalized content is a 409 Conflict. Reconcile against the latest authoritative state and allocate a newer revision; do not keep retrying the conflicting pair. An older revision is reported as a stale skipped snapshot rather than allowed to overwrite newer state. An unknown or unmapped scope_key fails closed for authorization and should be treated as a configuration alert.
If delivery times out or receives a transient 5xx, retry the exact normalized snapshot with the same revision. If the upstream source fetch failed, omit the window field entirely; never substitute an empty array.
Retry guidance
Retry these with bounded exponential backoff:
429500502503504
Do not blindly retry these:
400401403404409422
For access control, always treat timeout and offline responses as failed unlocks unless your own access rules and safety processes say otherwise.
Support details to include
When contacting support, include:
- Endpoint and HTTP method.
- Timestamp and timezone.
- Partner key ID, not the secret key.
- Facility identifier used.
- Source record ID from your system.
- Response status and body.
event_idorcommand_idwhere available.
{
"success": false,
"message": "Request body.members[0].member_id is required"
}
Some domain-specific endpoints return an explicit status and reason. For example, access-control failures include fields such as reason, door_id, facility_id, and event_id.
{
"status": "failed",
"reason": "device_offline",
"door_id": "577393ce6ec584aa4e2e2b1d1e73934c",
"facility_id": "550e8400-e29b-41d4-a716-446655440000",
"event_id": "2d98e9ff-3a2a-46d4-a22f-5f23f7d75b11"
}
400 Bad Request
The request is malformed or missing required fields. Fix the request before retrying.
401 Unauthorized
Authentication failed. Check that you are using HTTP Basic Auth with your API secret key as the username and an empty password.
403 Forbidden
The key is valid but the operation is not allowed. Common causes include using a read-only key for a write operation or attempting to access a facility where your integration is not enabled.
404 Not Found
The requested resource was not found or is not available to your integration.
409 Conflict
The request conflicts with the current resource state. For access control, this can include a Door/Gate Controller being in maintenance mode.
422 Unprocessable Entity
The request is valid JSON but fails semantic validation. Fix the data before retrying.
429 Too Many Requests
The integration exceeded the documented rate limit. Retry with exponential backoff.
500 Internal Server Error
Performance Hub could not complete the request because of an internal error. Retry with backoff. If the problem continues, contact support with the endpoint, timestamp, and response body.
503 Service Unavailable
A dependent service or device is unavailable. For Door/Gate Controller unlocks, this can mean the controller is offline.
504 Gateway Timeout
The operation timed out. For Door/Gate Controller unlocks, this means the unlock was not confirmed before the timeout window.
Versioning
The Partner API is versioned in the URL. The current version is:
Change policy
Performance Hub aims to make additive, backward-compatible changes wherever possible.
Examples of backward-compatible changes:
- Adding a new optional field to a request.
- Adding a new field to a response.
- Adding a new endpoint.
- Adding a new enum value where clients are expected to handle unknown values.
- Improving validation messages without changing the status code contract.
access_time_windows and access_time_windows_revision are additive Member request fields. Existing clients that omit them keep the stored Access Time Window snapshot unchanged. This omission rule preserves partial-upsert compatibility; omission is not equivalent to sending an empty array.
Examples of breaking changes:
- Removing or renaming a field.
- Changing the type of a field.
- Making an optional request field required.
- Removing an endpoint.
- Changing the meaning of an existing status code or domain-specific
reason.
Deprecations
When a breaking change is required, Performance Hub will avoid changing the existing version in place where possible. Instead, we will introduce a new version or migration path and communicate the change to affected partners.
Deprecation notices can include:
- A changelog entry.
- Direct partner communication.
- Notes in the Partner Console.
- Migration guidance in the docs.
Client recommendations
Build clients that tolerate additive changes:
- Ignore response fields you do not use.
- Treat unknown enum values as unsupported rather than crashing.
- Avoid strict response equality checks.
- Log unrecognized values so you can update your integration intentionally.
For revisioned Member partitions such as Access Time Windows, persist the submitted revision and normalized payload until the outcome is known. Reusing a revision for changed content is a conflict, while replaying the same content is idempotent. See Access Time Windows.
OpenAPI specifications
Use the published OpenAPI specifications to generate clients, validate requests, and track API shape changes over time.
https://partner-api.performancehub.co/partner/v1/
Changelog
This changelog records notable Partner API and Partner Console changes that affect integration behaviour.
2026-09-09
2026-05-06
2026-05-01
Change categories
Future entries use these categories:
- Added - new endpoint, field, guide, or capability.
- Changed - behaviour or documentation changed without removing support.
- Deprecated - feature remains available but has a planned migration path.
- Removed - feature or endpoint removed.
- Fixed - bug fix or documentation correction.
- Security - authentication, authorization, data handling, or key-management change.
Added
- Added vendor-neutral Member
access_time_windowssnapshots with mixed absolute and recurring weekly windows. - Added
access_time_windows_revisionordering, idempotent replay, conflict, stale-skip, and explicit-revocation semantics. - Added IANA timezone, daylight-saving, logical
scope_keymapping, source-freshness, producer implementation, and certification guidance.
Added
- Added partner program documentation covering partner types, onboarding, best practices, go-live readiness, errors, versioning, and support.
- Added a dedicated Become a Partner enquiry flow for prospective technical and commercial partners.
- Added documentation for how approved partner integrations appear inside the Performance Hub partner directory in the core Performance Hub platform.
Added
- Added Door/Gate Controller access-control documentation, including list, status, and unlock operations.
- Added synchronous unlock behaviour documentation: the API returns success only after the on-site controller confirms the relay physically unlocked.
- Added optional
idempotency_keysupport documentation for Door/Gate Controller unlock requests.