API, webhooks and Azure AD

Want to connect OfficeMaps to your own systems? This article shows you how to create API keys, set up webhooks, and manage the permissions the OfficeMaps app has in Microsoft Entra.

Note: The API needs a plan above Essentials. To upgrade, contact sales@officemaps.com.

Use the API

The OfficeMaps REST API lets you add, update, delete and search people. The full reference, with every endpoint, is at https://api.officemaps.io/.

Create an API key

  1. Click Admin, then Administration, then API Keys.

    att_0_for_1437138962.jpeg
    API Keys.
  2. Add a key with a clear Label and an Expiry date.

Each key shows when it was created and by whom. Untick Active to pause a key. Revoke a key to stop it for good; revoked keys can’t be turned back on, so you’ll need a new one.

When a call works

Every call must include your API key. A call only works when your subscription is current, and the key is active, not revoked and not expired.

Limits

You can make up to 1,000 calls a month and 10 calls a second. Over that, you’ll get an HTTP 429 response, so build in retries with a back-off. Need more? Email support@officemaps.com.

We may pause or revoke a key that’s used outside its intended purpose or our Terms of Service.

Set up webhooks

Webhooks send your app a message the moment something happens in OfficeMaps.

Event

When it’s sent

ReservationCreated

A desk or room is booked

ReservationCancelled

A booking is cancelled

CheckedIn

Someone checks in

CheckedOut

Someone checks out

PersonStatusChanged

Someone’s work status changes

Add a webhook

  1. Click Admin, then Administration, then Settings, then Webhooks.

    image-20260120-030437.png
    The webhook list.
  2. Click + Add and fill in the panel:

    • Name: a unique name.

    • Webhook URL: your HTTPS endpoint.

    • Secret Key: click Generate, or enter your own (16–256 characters). Store it safely.

    • Events to Send: choose at least one.

  3. Click Save.

    image-20260120-030449.png
    Add a webhook.

To change or delete a webhook, click its pencil icon. To check your endpoint, click Test Webhook.

Webhook Payload Structure

HTTP Request

OfficeMaps sends webhooks as HTTP POST requests with the following characteristics:

Property

Value

Method

POST

Content-Type

application/json

X-Signature

HMAC-SHA256 signature (lowercase hexadecimal)

Payload Wrapper

All webhook payloads follow this structure:

{
  "id": "string",
  "type": "string",
  "timestamp": "string",
  "data": { }
}

Field

Type

Description

id

string

Unique event identifier (UUID)

type

string

Event type name (e.g., ReservationCreated, CheckedIn)

timestamp

string

ISO 8601 timestamp when the event occurred (UTC)

data

object

Event-specific payload data


Event Types

ReservationCreated

Triggered when a new desk or room reservation is created.

Data Fields

Field

Type

Description

personAssetId

string (UUID)

Unique identifier for the person-asset link

personId

string (UUID)

ID of the person making the reservation

assetId

string (UUID)

ID of the reserved asset (desk/room)

assetName

string

Name of the asset

reservationStartTimeUtc

string (ISO 8601)

Reservation start time in UTC

reservationEndTimeUtc

string (ISO 8601)

Reservation end time in UTC

reservationStartTimeLocal

string (ISO 8601)

Reservation start time in local timezone

reservationEndTimeLocal

string (ISO 8601)

Reservation end time in local timezone

timezoneIana

string

IANA timezone identifier (e.g., Australia/Sydney)

siteLocationId

string (UUID)

Site/building ID

siteLocationName

string

Site/building name

siteLevelId

string (UUID)

Floor/level ID

siteLevelName

string

Floor/level name

city

string

City

region

string

Region/State

country

string

Country

floorName

string

Floor name

neighbourhoodName

string

Neighbourhood/zone name

personEmail

string

Person's email address

personFullName

string

Person's full name

reservationIsToday

boolean

Whether the reservation is for today


ReservationCancelled

Triggered when a reservation is cancelled.

Data Fields

Field

Type

Description

personAssetId

string (UUID)

Unique identifier for the person-asset link

personId

string (UUID)

ID of the person who made the reservation

assetId

string (UUID)

ID of the reserved asset

assetName

string

Name of the asset

reservationStartTimeUtc

string (ISO 8601)

Original start time in UTC

reservationEndTimeUtc

string (ISO 8601)

Original end time in UTC

reservationStartTimeLocal

string (ISO 8601)

Original start time in local timezone

reservationEndTimeLocal

string (ISO 8601)

Original end time in local timezone

timezoneIana

string

IANA timezone identifier

siteLevelName

string

Floor/level name

floorName

string

Floor name

neighbourhoodName

string

Neighbourhood/zone name

personEmail

string

Person's email address

personFullName

string

Person's full name

cancelledAt

string (ISO 8601)

When the reservation was cancelled (UTC)

cancelledBy

string

Who cancelled the reservation


CheckedIn

Triggered when a user checks into an asset.

Data Fields

Field

Type

Description

personAssetId

string (UUID)

Unique identifier for the person-asset link

personId

string (UUID)

ID of the person checking in

assetId

string (UUID)

ID of the asset

assetName

string

Name of the asset

checkInTimeUtc

string (ISO 8601)

Check-in time in UTC

checkInTimeLocal

string (ISO 8601)

Check-in time in local timezone

expectedCheckOutTimeUtc

string (ISO 8601)

Expected check-out time in UTC

expectedCheckOutTimeLocal

string (ISO 8601)

Expected check-out time in local timezone

timezoneIana

string

IANA timezone identifier

timezoneDisplayName

string

Human-readable timezone name

siteLocationId

string (UUID)

Site/building ID

siteLocationName

string

Site/building name

siteLevelId

string (UUID)

Floor/level ID

siteLevelName

string

Floor/level name

city

string

City

region

string

Region/State

country

string

Country

personEmail

string

Person's email address

personFullName

string

Person's full name

firstName

string

Person's first name

lastName

string

Person's last name

fromReservation

boolean

Whether check-in was from a prior reservation


CheckedOut

Triggered when a user checks out of an asset.

Data Fields

Field

Type

Description

personAssetId

string (UUID)

Unique identifier for the person-asset link

personId

string (UUID)

ID of the person checking out

assetId

string (UUID)

ID of the asset

assetName

string

Name of the asset

checkOutTimeUtc

string (ISO 8601)

Check-out time in UTC

checkOutTimeLocal

string (ISO 8601)

Check-out time in local timezone

checkInTimeUtc

string (ISO 8601)

Original check-in time in UTC

checkInTimeLocal

string (ISO 8601)

Original check-in time in local timezone

timezoneIana

string

IANA timezone identifier

timezoneDisplayName

string

Human-readable timezone name

siteLocationId

string (UUID)

Site/building ID

siteLocationName

string

Site/building name

siteLevelId

string (UUID)

Floor/level ID

siteLevelName

string

Floor/level name

city

string

City

region

string

Region/State

country

string

Country

personEmail

string

Person's email address

personFullName

string

Person's full name

firstName

string

Person's first name

lastName

string

Person's last name


PersonStatusChanged

Triggered when a user's work status changes.

Data Fields

Field

Type

Description

personId

string (UUID)

ID of the person

personFullName

string

Person's full name

personEmail

string

Person's email address

oldPersonStatusId

string (UUID)

Previous status ID (null if no previous status)

oldPersonStatusName

string

Previous status name

oldWorkDayType

integer

Previous work day type (see below)

newPersonStatusId

string (UUID)

New status ID

newPersonStatusName

string

New status name

newWorkDayType

integer

New work day type (see below)

statusChangeMethod

integer

How the status was changed (see below)

statusChangedAtUtc

string (ISO 8601)

When the status changed (UTC)

personStatusComment

string

Optional comment on the status

personStatusExpiry

string (ISO 8601)

When the status expires (null if no expiry)

isStatusConfirmed

boolean

Whether the status has been confirmed

Work Day Type Values

Value

Description

0

Non-Work Day

1

Office

2

Remote

Status Change Method Values

Value

Description

0

Automatic

1

Manual (Web App)

2

Manual (Mobile)

3

On Check-In

4

On Check-Out

5

Notification Email


Signature Validation

To ensure webhook requests are authentic and haven't been tampered with, you should validate the X-Signature header.

How Signatures Are Computed

  1. The entire JSON payload body is used as input

  2. Your webhook secret is used as the HMAC key

  3. HMAC-SHA256 is computed

  4. The result is converted to a lowercase hexadecimal string

Validation Steps

  1. Read the raw request body (do not parse JSON first)

  2. Extract the X-Signature header value

  3. Compute HMAC-SHA256 of the raw body using your webhook secret

  4. Convert the hash to lowercase hexadecimal

  5. Compare your computed signature with the received X-Signature

Code Samples

C# (.NET)

C#
using System.Security.Cryptography;
using System.Text;

public class WebhookValidator
{
    public static bool ValidateSignature(string payload, string secret, string receivedSignature)
    {
        var keyBytes = Encoding.UTF8.GetBytes(secret);
        var payloadBytes = Encoding.UTF8.GetBytes(payload);

        using var hmac = new HMACSHA256(keyBytes);
        var hashBytes = hmac.ComputeHash(payloadBytes);
        var computedSignature = Convert.ToHexString(hashBytes).ToLowerInvariant();

        return computedSignature == receivedSignature;
    }
}

// Usage in ASP.NET Core controller:
[HttpPost("webhook")]
public async Task<IActionResult> HandleWebhook()
{
    using var reader = new StreamReader(Request.Body);
    var payload = await reader.ReadToEndAsync();

    var signature = Request.Headers["X-Signature"].FirstOrDefault();
    var secret = "your-webhook-secret";

    if (!WebhookValidator.ValidateSignature(payload, secret, signature))
    {
        return Unauthorized("Invalid signature");
    }

    var webhookEvent = JsonSerializer.Deserialize<WebhookPayload>(payload);
    // Process the event...

    return Ok();
}

Java

Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class WebhookValidator {

    public static boolean validateSignature(String payload, String secret, String receivedSignature) {
        try {
            Mac hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
            );
            hmac.init(secretKey);

            byte[] hashBytes = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
            String computedSignature = bytesToHex(hashBytes).toLowerCase();

            return computedSignature.equals(receivedSignature);
        } catch (Exception e) {
            return false;
        }
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

// Usage in Spring Boot controller:
@PostMapping("/webhook")
public ResponseEntity<String> handleWebhook(
        @RequestBody String payload,
        @RequestHeader("X-Signature") String signature) {

    String secret = "your-webhook-secret";

    if (!WebhookValidator.validateSignature(payload, secret, signature)) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
    }

    // Process the event...
    return ResponseEntity.ok("OK");
}

JavaScript (Node.js)

JavaScript
const crypto = require('crypto');

function validateSignature(payload, secret, receivedSignature) {
    const computedSignature = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');

    return computedSignature === receivedSignature;
}

// Usage with Express:
const express = require('express');
const app = express();

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const payload = req.body.toString('utf8');
    const signature = req.headers['x-signature'];
    const secret = 'your-webhook-secret';

    if (!validateSignature(payload, secret, signature)) {
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    // Process the event...

    res.status(200).send('OK');
});

Python

Python
import hmac
import hashlib

def validate_signature(payload: str, secret: str, received_signature: str) -> bool:
    computed_signature = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed_signature, received_signature)

# Usage with Flask:
from flask import Flask, request, abort
import json

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-Signature')
    secret = 'your-webhook-secret'

    if not validate_signature(payload, secret, signature):
        abort(401, 'Invalid signature')

    event = json.loads(payload)
    # Process the event...

    return 'OK', 200

Example Payloads

ReservationCreated Example

JSON
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "ReservationCreated",
  "timestamp": "2024-01-15T09:30:00Z",
  "data": {
    "personAssetId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "personId": "550e8400-e29b-41d4-a716-446655440000",
    "assetId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "assetName": "Desk A-101",
    "reservationStartTimeUtc": "2024-01-16T09:00:00Z",
    "reservationEndTimeUtc": "2024-01-16T17:00:00Z",
    "reservationStartTimeLocal": "2024-01-16T20:00:00",
    "reservationEndTimeLocal": "2024-01-17T04:00:00",
    "timezoneIana": "Australia/Sydney",
    "siteLocationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "siteLocationName": "Sydney HQ",
    "siteLevelId": "8f14e45f-ceea-467f-a0e8-14db0148c3a1",
    "siteLevelName": "Level 5",
    "city": "Sydney",
    "region": "NSW",
    "country": "Australia",
    "floorName": "Floor 5",
    "neighbourhoodName": "Engineering Zone",
    "personEmail": "john.smith@example.com",
    "personFullName": "John Smith",
    "reservationIsToday": false
  }
}

ReservationCancelled Example

JSON
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "type": "ReservationCancelled",
  "timestamp": "2024-01-15T10:15:00Z",
  "data": {
    "personAssetId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "personId": "550e8400-e29b-41d4-a716-446655440000",
    "assetId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "assetName": "Desk A-101",
    "reservationStartTimeUtc": "2024-01-16T09:00:00Z",
    "reservationEndTimeUtc": "2024-01-16T17:00:00Z",
    "reservationStartTimeLocal": "2024-01-16T20:00:00",
    "reservationEndTimeLocal": "2024-01-17T04:00:00",
    "timezoneIana": "Australia/Sydney",
    "siteLevelName": "Level 5",
    "floorName": "Floor 5",
    "neighbourhoodName": "Engineering Zone",
    "personEmail": "john.smith@example.com",
    "personFullName": "John Smith",
    "cancelledAt": "2024-01-15T10:15:00Z",
    "cancelledBy": "john.smith@example.com"
  }
}

CheckedIn Example

JSON
{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "type": "CheckedIn",
  "timestamp": "2024-01-16T09:05:00Z",
  "data": {
    "personAssetId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "personId": "550e8400-e29b-41d4-a716-446655440000",
    "assetId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "assetName": "Desk A-101",
    "checkInTimeUtc": "2024-01-16T09:05:00Z",
    "checkInTimeLocal": "2024-01-16T20:05:00",
    "expectedCheckOutTimeUtc": "2024-01-16T17:00:00Z",
    "expectedCheckOutTimeLocal": "2024-01-17T04:00:00",
    "timezoneIana": "Australia/Sydney",
    "timezoneDisplayName": "Australian Eastern Daylight Time",
    "siteLocationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "siteLocationName": "Sydney HQ",
    "siteLevelId": "8f14e45f-ceea-467f-a0e8-14db0148c3a1",
    "siteLevelName": "Level 5",
    "city": "Sydney",
    "region": "NSW",
    "country": "Australia",
    "personEmail": "john.smith@example.com",
    "personFullName": "John Smith",
    "firstName": "John",
    "lastName": "Smith",
    "fromReservation": true
  }
}

CheckedOut Example

JSON
{
  "id": "d4e5f6a7-b8c9-0123-def0-234567890123",
  "type": "CheckedOut",
  "timestamp": "2024-01-16T17:30:00Z",
  "data": {
    "personAssetId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "personId": "550e8400-e29b-41d4-a716-446655440000",
    "assetId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "assetName": "Desk A-101",
    "checkOutTimeUtc": "2024-01-16T17:30:00Z",
    "checkOutTimeLocal": "2024-01-17T04:30:00",
    "checkInTimeUtc": "2024-01-16T09:05:00Z",
    "checkInTimeLocal": "2024-01-16T20:05:00",
    "timezoneIana": "Australia/Sydney",
    "timezoneDisplayName": "Australian Eastern Daylight Time",
    "siteLocationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "siteLocationName": "Sydney HQ",
    "siteLevelId": "8f14e45f-ceea-467f-a0e8-14db0148c3a1",
    "siteLevelName": "Level 5",
    "city": "Sydney",
    "region": "NSW",
    "country": "Australia",
    "personEmail": "john.smith@example.com",
    "personFullName": "John Smith",
    "firstName": "John",
    "lastName": "Smith"
  }
}

PersonStatusChanged Example

JSON
{
  "id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
  "type": "PersonStatusChanged",
  "timestamp": "2024-01-16T08:45:00Z",
  "data": {
    "personId": "550e8400-e29b-41d4-a716-446655440000",
    "personFullName": "John Smith",
    "personEmail": "john.smith@example.com",
    "oldPersonStatusId": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
    "oldPersonStatusName": "Remote",
    "oldWorkDayType": 2,
    "newPersonStatusId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "newPersonStatusName": "In Office",
    "newWorkDayType": 1,
    "statusChangeMethod": 1,
    "statusChangedAtUtc": "2024-01-16T08:45:00Z",
    "personStatusComment": null,
    "personStatusExpiry": null,
    "isStatusConfirmed": true
  }
}

Remove permissions in Microsoft Entra

  1. In the Entra admin center, open Enterprise applications.

    att_0_for_1435598899.jpeg
    Enterprise applications.
  2. Click the OfficeMaps application, then the Permissions tab.

    att_1_for_1435598899.jpeg
    The OfficeMaps application.
    att_2_for_1435598899.jpeg
    The Permissions tab.
  3. Next to the permission, click …, then Revoke permission, then Yes, revoke.

    att_3_for_1435598899.jpeg
    Revoke permission.
    att_4_for_1435598899.jpeg
    Confirm.

OfficeMaps can no longer act with that permission. To restore the original permissions, click Consent on the integration in OfficeMaps.