REST API Structure

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.


Quick Reference

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

Authentication

All API calls require authentication, except the token endpoint itself.

Getting an Access Token

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

Using the Access Token

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)

Refreshing the Token

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_in to avoid expiration during operations.


API Base URL

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
  ...

Constructing Resource URLs

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"

HTTP Methods

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)

Example: CRUD Operations on People

Create (POST)

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

Read (GET)

# 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()

Update (PUT)

# 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()

Delete (DELETE)

response = requests.delete(
    f"https://api.us.acresecurity.cloud{person['Href']}",
    headers=headers
)

if response.status_code == 204:
    print("Person deleted successfully")

Strong Typing ($type Property)

Critical for non-.NET developers: Every object sent to or received from the API includes type information via the $type property.

Format

Feenics.Keep.WebApi.Model.{TypeName}, Feenics.Keep.WebApi.Model

Common Types

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

Polymorphic Types

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

Pagination

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}
)

Pagination Parameters

Parameter Default Description
page 0 Page number (0-indexed)
pageSize 20 Items per page (max 1000)

Iterating All Pages

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

Searching

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()

Search with Child Instances

response = requests.post(
    f"https://api.us.acresecurity.cloud{instance['Href']}/search",
    headers=headers,
    params={
        'page': 0,
        'pageSize': 100,
        'includeChildInstances': 'true'
    },
    data='"{\\"_t\\": \\"Person\\"}"'
)

Object Relationships

Objects are connected via the ObjectLinks property. To manage relationships, use the /connections endpoint.

Adding a Relationship (POST)

# 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)

Removing a Relationship (DELETE)

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)

Common Relationship Types

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

Error Handling

HTTP Status Codes

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 Response Format

{
    "Error": "CardIsAlreadyAssigned",
    "Message": "Card number 12345 is already assigned to another person"
}

Python Error Handling

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

Complete Example: Python Integration

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")

Additional Resources