This guide describes the acre Access Control API from the perspective of HTTP and REST principles. If you’re building integrations with Python, JavaScript, PHP, Node.js, or any non-.NET language, this is essential reading.
For .NET Developers: While this guide is valuable for understanding the API, you’ll likely use the C# Wrapper Library which handles these details for you. In which case, get started here.
| Endpoint | Purpose |
|---|---|
POST /token |
Authentication (get access token) |
GET /api |
Get current instance |
GET /api/f/{instanceKey}/people |
List people |
POST /api/f/{instanceKey}/people |
Create person |
GET /api/f/{instanceKey}/people/{key} |
Get specific person |
PUT /api/f/{instanceKey}/people/{key} |
Update person |
DELETE /api/f/{instanceKey}/people/{key} |
Delete person |
All API calls require authentication, except the token endpoint itself.
Endpoint: POST https://api.us.acresecurity.cloud/token
Request:
import requests
import json
response = requests.post('https://api.us.acresecurity.cloud/token',
data={
'grant_type': 'password',
'client_id': 'consoleApp',
'client_secret': 'consoleSecret',
'username': 'myinstance\\myusername', # Note: backslash escaped
'password': 'mypassword',
'instance': 'myinstance'
}
)
result = response.json()
access_token = result['access_token']
refresh_token = result['refresh_token']
expires_in = result['expires_in'] # Seconds until expiration
print(f"Token expires in {expires_in} seconds")
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 86399,
"refresh_token": "5bd30016a3ac160cf88835f3",
"userName": "myinstance\\myusername",
"instance": "5bcde6c5a3ac1600485092a0",
".issued": "Wed, 22 Jan 2026 11:52:53 GMT",
".expires": "Thu, 23 Jan 2026 11:52:53 GMT"
}
Include the token in the Authorization header for all subsequent requests:
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.get('https://api.us.acresecurity.cloud/api', headers=headers)
Before the token expires, use the refresh token to get a new access token:
response = requests.post('https://api.us.acresecurity.cloud/token',
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'client_id': 'consoleApp',
'client_secret': 'consoleSecret'
}
)
new_tokens = response.json()
access_token = new_tokens['access_token']
refresh_token = new_tokens['refresh_token'] # Save the new refresh token
Best Practice: Set a timer to refresh the token at 75% of
expires_into avoid expiration during operations.
The API is available in multiple regions:
| Region | Base URL |
|---|---|
| United States | https://api.us.acresecurity.cloud |
| Europe | https://api.eu.acresecurity.cloud |
| Canada | https://api.ca.acresecurity.cloud |
The API follows HATEOAS principles. Start at the base endpoint and navigate using the links provided:
# Step 1: Get the current instance
response = requests.get('https://api.us.acresecurity.cloud/api', headers=headers)
instance = response.json()
print(f"Instance: {instance['CommonName']}")
print(f"Href: {instance['Href']}")
# Step 2: Find available resources from Links
for link in instance['Links']:
print(f" {link['Relation']}: {link['Anchor']['Href']}")
Output:
Instance: My Organization
Href: /api/f/5bcde6c5a3ac1600485092a0
People: people
Controllers: controllers
AccessLevels: accesslevels
Schedules: schedules
...
Combine the instance Href with the link’s Anchor.Href:
instance_href = instance['Href'] # /api/f/5bd30016a3ac160cf88835f3
# People resource URL
people_url = f"https://api.us.acresecurity.cloud{instance_href}/people"
# Controllers resource URL
controllers_url = f"https://api.us.acresecurity.cloud{instance_href}/controllers"
| Method | Purpose | Request Body | Response |
|---|---|---|---|
GET |
Retrieve object(s) | None | Object or array |
POST |
Create new object | New object JSON | Created object |
PUT |
Update existing object | Full object JSON | Updated object |
DELETE |
Remove object | None | Empty (204) |
new_person = {
"$type": "Feenics.Keep.WebApi.Model.PersonInfo, Feenics.Keep.WebApi.Model",
"CommonName": "Smith, John",
"GivenName": "John",
"Surname": "Smith",
"InFolderKey": instance['Key']
}
response = requests.post(
f"https://api.us.acresecurity.cloud{instance['Href']}/people",
headers=headers,
json=new_person
)
created_person = response.json()
print(f"Created person with Key: {created_person['Key']}")
# Get all people (paginated)
response = requests.get(
f"https://api.us.acresecurity.cloud{instance['Href']}/people",
headers=headers
)
people = response.json()
# Get specific person
response = requests.get(
f"https://api.us.acresecurity.cloud{instance['Href']}/people/{person_key}",
headers=headers
)
person = response.json()
# Modify the person
person['GivenName'] = "Jonathan"
response = requests.put(
f"https://api.us.acresecurity.cloud{person['Href']}",
headers=headers,
json=person
)
updated_person = response.json()
response = requests.delete(
f"https://api.us.acresecurity.cloud{person['Href']}",
headers=headers
)
if response.status_code == 204:
print("Person deleted successfully")
Critical for non-.NET developers: Every object sent to or received from the API includes type information via the $type property.
Feenics.Keep.WebApi.Model.{TypeName}, Feenics.Keep.WebApi.Model
| Object | $type Value |
|---|---|
| Person | Feenics.Keep.WebApi.Model.PersonInfo, Feenics.Keep.WebApi.Model |
| Access Level | Feenics.Keep.WebApi.Model.AccessLevelInfo, Feenics.Keep.WebApi.Model |
| Schedule | Feenics.Keep.WebApi.Model.ScheduleInfo, Feenics.Keep.WebApi.Model |
| Instance | Feenics.Keep.WebApi.Model.InstanceInfo, Feenics.Keep.WebApi.Model |
| Mercury Controller | Feenics.Keep.WebApi.Model.Lp1502Info, Feenics.Keep.WebApi.Model |
| Mercury Reader | Feenics.Keep.WebApi.Model.MercuryReaderInfo, Feenics.Keep.WebApi.Model |
Some properties can contain different sub-types. For example, Addresses can contain:
{
"Addresses": [
{
"$type": "Feenics.Keep.WebApi.Model.MailingAddressInfo, Feenics.Keep.WebApi.Model",
"Street": "123 Main St",
"City": "Ottawa",
"Province": "Ontario",
"PostalCode": "K1G 5H9",
"Country": "Canada",
"Type": "Work"
},
{
"$type": "Feenics.Keep.WebApi.Model.EmailAddressInfo, Feenics.Keep.WebApi.Model",
"MailTo": "john.smith@example.com",
"Type": "Work"
},
{
"$type": "Feenics.Keep.WebApi.Model.PhoneInfo, Feenics.Keep.WebApi.Model",
"Number": "16135551234",
"Type": "Work"
}
]
}
List endpoints return paginated results:
# First page (20 items by default)
response = requests.get(
f"https://api.us.acresecurity.cloud{instance['Href']}/people",
headers=headers
)
# With custom page size
response = requests.get(
f"https://api.us.acresecurity.cloud{instance['Href']}/people",
headers=headers,
params={'page': 0, 'pageSize': 100}
)
| Parameter | Default | Description |
|---|---|---|
page |
0 | Page number (0-indexed) |
pageSize |
20 | Items per page (max 1000) |
def get_all_people(base_url, instance_href, headers):
all_people = []
page = 0
page_size = 100
while True:
response = requests.get(
f"{base_url}{instance_href}/people",
headers=headers,
params={'page': page, 'pageSize': page_size}
)
people = response.json()
if not people: # Empty page = no more data
break
all_people.extend(people)
page += 1
return all_people
Use the search endpoint for complex queries:
# MongoDB query syntax
query = '{"_t": "Person", "Surname": "Smith"}'
response = requests.post(
f"https://api.us.acresecurity.cloud{instance['Href']}/search",
headers=headers,
params={'page': 0, 'pageSize': 100},
data=f'"{query}"' # Note: query is wrapped in quotes
)
results = response.json()
response = requests.post(
f"https://api.us.acresecurity.cloud{instance['Href']}/search",
headers=headers,
params={
'page': 0,
'pageSize': 100,
'includeChildInstances': 'true'
},
data='"{\\"_t\\": \\"Person\\"}"'
)
Objects are connected via the ObjectLinks property. To manage relationships, use the /connections endpoint.
# Assign access level to person
instance_key = "5bd30016a3ac160cf88835f3"
person_key = "5bd5d98ef0794c529032b7f1"
access_level_key = "5bd5d98ff1794c509032b7fa"
url = (
f"https://api.us.acresecurity.cloud/api/f/{instance_key}"
f"/people/{person_key}/connections/AccessLevel"
f"?relatedKey={access_level_key}&isOneToOne=false"
)
response = requests.post(url, headers=headers)
url = (
f"https://api.us.acresecurity.cloud/api/f/{instance_key}"
f"/people/{person_key}/connections/AccessLevel/{access_level_key}"
)
response = requests.delete(url, headers=headers)
| Relation | From | To | One-to-One |
|---|---|---|---|
AccessLevel |
Person | AccessLevel | No |
BadgeType |
Person | BadgeType | Yes |
Schedule |
AccessLevel | Schedule | No |
Reader |
AccessLevel | Reader | No |
Controller |
Downstream | Controller | Yes |
Door |
Reader | Door | Yes |
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 201 | Created | Object created successfully |
| 204 | No Content | Delete successful, Save successful, but unchanged |
| 400 | Bad Request | Check request body/params |
| 401 | Unauthorized | Token expired; refresh or re-authenticate |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Object doesn’t exist |
| 409 | Conflict | Duplicate or constraint violation |
| 500 | Server Error | Retry or contact support, unhandled error |
{
"Error": "CardIsAlreadyAssigned",
"Message": "Card number 12345 is already assigned to another person"
}
def safe_api_call(method, url, headers, **kwargs):
try:
response = method(url, headers=headers, **kwargs)
response.raise_for_status()
return response.json() if response.content else None
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
# Token expired - refresh and retry
refresh_token()
return safe_api_call(method, url, headers, **kwargs)
elif e.response.status_code == 409:
error = e.response.json()
print(f"Conflict: {error.get('Message', 'Unknown error')}")
else:
print(f"API Error: {e.response.status_code} - {e.response.text}")
raise
import requests
from datetime import datetime, timedelta
class AcreClient:
def __init__(self, base_url, instance_name, username, password):
self.base_url = base_url
self.instance_name = instance_name
self.username = username
self.password = password
self.access_token = None
self.refresh_token = None
self.token_expires = None
self.instance = None
def login(self):
response = requests.post(f"{self.base_url}/token", data={
'grant_type': 'password',
'client_id': 'consoleApp',
'client_secret': 'consoleSecret',
'username': f"{self.instance_name}\\{self.username}",
'password': self.password,
'instance': self.instance_name
})
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.refresh_token = data['refresh_token']
self.token_expires = datetime.utcnow() + timedelta(seconds=data['expires_in'])
# Get instance info
self.instance = self._get('/api')
return True
def _headers(self):
# Auto-refresh if token is about to expire
if self.token_expires and datetime.utcnow() > self.token_expires - timedelta(minutes=5):
self._refresh_token()
return {
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json'
}
def _refresh_token(self):
response = requests.post(f"{self.base_url}/token", data={
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
'client_id': 'consoleApp',
'client_secret': 'consoleSecret'
})
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.refresh_token = data['refresh_token']
self.token_expires = datetime.utcnow() + timedelta(seconds=data['expires_in'])
def _get(self, path, params=None):
url = f"{self.base_url}{path}" if path.startswith('/') else path
response = requests.get(url, headers=self._headers(), params=params)
response.raise_for_status()
return response.json()
def _post(self, path, data):
url = f"{self.base_url}{path}" if path.startswith('/') else path
response = requests.post(url, headers=self._headers(), json=data)
response.raise_for_status()
return response.json()
def get_people(self, page=0, page_size=100):
return self._get(f"{self.instance['Href']}/people", {
'page': page,
'pageSize': page_size
})
def create_person(self, given_name, surname, **kwargs):
person = {
"$type": "Feenics.Keep.WebApi.Model.PersonInfo, Feenics.Keep.WebApi.Model",
"CommonName": f"{surname}, {given_name}",
"GivenName": given_name,
"Surname": surname,
"InFolderKey": self.instance['Key'],
**kwargs
}
return self._post(f"{self.instance['Href']}/people", person)
def search(self, query, page=0, page_size=100):
response = requests.post(
f"{self.base_url}{self.instance['Href']}/search",
headers=self._headers(),
params={'page': page, 'pageSize': page_size},
data=f'"{query}"'
)
response.raise_for_status()
return response.json()
# Usage
if __name__ == "__main__":
client = AcreClient(
base_url="https://api.us.acresecurity.cloud",
instance_name="myinstance",
username="admin",
password="password123"
)
client.login()
print(f"Connected to: {client.instance['CommonName']}")
# Create a person
new_person = client.create_person("John", "Smith")
print(f"Created: {new_person['CommonName']} ({new_person['Key']})")
# Search for people
smiths = client.search('{"_t": "Person", "Surname": "Smith"}')
print(f"Found {len(smiths)} people named Smith")