MENU navbar-image

Introduction

About

🚧 BETA: The Shlott API is in an early release BETA testing phase and is subject to change without notice. Please do not rely on it for anything critical.

This is the documentation for the Shlott REST API. The API is the primary way to interact with Shlott programmatically, and can be used to build integrations and custom tooling for your flying club or aviation organisation. This page documents the available authentication methods, all API endpoints, example request payloads, and the expected HTTP responses.

API access is included with Shlott Pro groups. Every group can use Shlott's core booking features on the free plan, and Pro adds extras such as the API, while helping to fund the continued development of Shlott. If your group is on the free plan and you would like to use the API, you can upgrade to Pro at any time from the Subscription page in Shlott.

Overview

The Shlott REST API is a token-based API that sits alongside the Shlott web platform, providing a focused integration layer for third-party tools. The API exposes a curated set of endpoints covering calendars, bookings, and club membership — with more endpoints being added over time.

The API is secured using token-based authentication via Laravel Sanctum. Most endpoints are protected and require a valid API key. To obtain an API key, log into Shlott, go to Manage Account, click the API tile and then click Generate API Key. The key must be included in the Authorization header of every subsequent request, preceded by Bearer — for example: Authorization: Bearer your-api-key-here.

Each API key is specific to the group it was created for, so switch to the group in Shlott before generating the key. A key only works for that group, so to use the API with several groups, create a key in each one (you can create as many keys as you need). A key stops working if the group stops being Pro, or if you leave or are removed from the group. If a request includes a group_id it must be the group the key was created for; if it is left out, that group is used.

API keys expire 30 days after they are created, after which requests return 401 Unauthenticated and you must generate a new one. To invalidate a key sooner, remove it from the API page in Shlott.

The API accepts and returns JSON only.

Getting started

  1. Generate an API key in Shlott (see above) while you are in the group you want to use.
  2. Send a request with the key in the Authorization header. This one lists a day's bookings on the calendar:
curl "https://api.shlott.com/v1/calendar/day?date=2025-06-21" \
  -H "Authorization: Bearer your-api-key-here" \
  -H "Accept: application/json"

You do not need to send group_id, because the key already belongs to one group. If you do send it, it must be that group. A 200 response contains the day's resources and bookings as JSON. If you get a 401, 403 or 422, see Errors below.

Errors

Every error response is JSON and includes a human-readable message. The status code tells you what went wrong:

Status Meaning
401 The API key is missing, wrong, revoked or has expired (keys last 30 days). Body: {"message": "Unauthenticated."}
403 The key is valid but is not allowed to do this. For example the key belongs to a different group, the group is no longer Pro, or the action needs a group administrator.
404 The thing you asked for does not exist, or is not in your group.
422 The request failed validation. errors lists each field and what is wrong with it.
429 Too many requests. Wait for the number of seconds in the Retry-After header, then try again.

Notice: the message text is written for people. It can change and may be translated, so never match on it in your code. Use the HTTP status code instead.

Validation errors look like this:

{
  "message": "The date field is required.",
  "errors": { "date": ["The date field is required."] }
}

Rate limits

Each user can make up to 60 requests per minute across the API, however many keys they have. Some endpoints that send emails or delete data have tighter limits, which are noted on the endpoint. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers so you can see how much of your allowance is left. When you go over, you get a 429 response with a Retry-After header. Please do not poll the API repeatedly: fetch what you need and cache it.

Pagination

Most list endpoints return everything in one response. Endpoints that can return a lot of data, currently List Members, are paginated: send page (from 1) and per_page (1 to 100, default 25), and read total, page, per_page and has_more in the response. Keep requesting the next page until has_more is false. Each endpoint's documentation says whether it is paginated.

Support

Support for the Shlott API is limited. While we are happy to assist with questions about specific endpoints or unexpected behaviour, we are unable to provide assistance with writing code, building integrations, or connecting the API to your own website or application. Developers are expected to have the necessary technical knowledge to work with a REST API independently.

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_AUTH_KEY}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

You can obtain an API key by logging into Shlott, going to Manage Account and clicking the API tile, then clicking Generate API Key.

Calendar

Calendar Sort Order

requires authentication

Categories and roles in the order the calendar shows them. count is the number of live objects (categories) or members (roles) in each.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/sort-order"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/sort-order';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/sort-order'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Order):


{
    "data": [
        {
            "id": 3,
            "type": "resource",
            "name": "Courts",
            "count": 4
        },
        {
            "id": 2,
            "type": "role",
            "name": "Coach",
            "count": 6
        }
    ]
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/sort-order

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Save Calendar Sort Order

requires authentication

The complete new order of categories and roles, first to last. Hidden ones are moved after them, as on the website.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/sort-order"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "order": [
        "architecto"
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/sort-order';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'order' => ['architecto'],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/sort-order'
payload = {
    "group_id": 5,
    "order": [
        "architecto"
    ]
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "The order has been saved."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

PUT v1/manage/sort-order

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

order   string[]     

Every visible category and role, in the new order.

*   object  optional    
id   integer     

Example: 3

type   string     

resource (category) or role. Example: resource

Get Calendar Settings

requires authentication

The values on the website's calendar settings page, plus the objects that can be picked as the default month-view object (value is resource_<id> or member_<id>; 0 means none).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/calendar-settings"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/calendar-settings';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/calendar-settings'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Settings):


{
    "calendar_intervals": 2,
    "hide_member_details_on_calendar": false,
    "hide_unavailable_all_day_rows": false,
    "flag_booking_if_member_has_not_booked_in": 0,
    "week_starts_on": 1,
    "default_overview_object": "0",
    "overview_amber_limit": 3,
    "overview_red_limit": 5,
    "objects": [
        {
            "value": "resource_8",
            "label": "Court 1 (Courts)"
        }
    ]
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/calendar-settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Update Calendar Settings

requires authentication

Same rules as the website: the calendar interval must suit the group's opening times, the amber limit must be below the red limit, and the default object must belong to the group.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/calendar-settings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "calendar_intervals": 2,
    "hide_member_details_on_calendar": false,
    "hide_unavailable_all_day_rows": false,
    "flag_booking_if_member_has_not_booked_in": 0,
    "week_starts_on": 1,
    "default_overview_object": "0",
    "overview_amber_limit": 3,
    "overview_red_limit": 5
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/calendar-settings';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'calendar_intervals' => 2,
            'hide_member_details_on_calendar' => false,
            'hide_unavailable_all_day_rows' => false,
            'flag_booking_if_member_has_not_booked_in' => 0,
            'week_starts_on' => 1,
            'default_overview_object' => '0',
            'overview_amber_limit' => 3,
            'overview_red_limit' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/calendar-settings'
payload = {
    "group_id": 5,
    "calendar_intervals": 2,
    "hide_member_details_on_calendar": false,
    "hide_unavailable_all_day_rows": false,
    "flag_booking_if_member_has_not_booked_in": 0,
    "week_starts_on": 1,
    "default_overview_object": "0",
    "overview_amber_limit": 3,
    "overview_red_limit": 5
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Settings updated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Invalid):


{
    "message": "The overview red limit field must be greater than overview amber limit.",
    "errors": {
        "overview_red_limit": [
            "..."
        ]
    }
}
 

Request   

PUT v1/manage/calendar-settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

calendar_intervals   integer     

1 (hour), 2 (30 minutes) or 4 (15 minutes). Example: 2

hide_member_details_on_calendar   boolean     

Example: false

hide_unavailable_all_day_rows   boolean     

Example: false

flag_booking_if_member_has_not_booked_in   integer     

0 (off) to 12 months. Example: 0

week_starts_on   integer     

0 (Sunday) or 1 (Monday). Example: 1

default_overview_object   string     

0, resource_<id> or member_<id>. Example: 0

overview_amber_limit   integer     

Example: 3

overview_red_limit   integer     

Example: 5

Get Calendar Day

requires authentication

Returns all resources and role members (e.g. instructors) for the specified group on the given date, together with their bookings and unavailable time blocks. The response also includes the group's opening hours and minimum booking duration for that day so a client can render a full calendar grid without making additional requests.

Resources and role members that are marked as not visible, or whose availability record shows them as off for the requested day, are omitted. Supply resource_id to narrow the result to a single row.

When the group hides member details on the calendar, a non-admin sees "Booked" as the slot_text, and no booked_for, reason or note, on bookings they are not involved in (they did not create it, and it is not booked for, booked with or shared with them). Admins always see everything. Requests are limited to 60 a minute per user.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/calendar/day"
);

const params = {
    "date": "2025-06-21",
    "group_id": "5",
    "resource_id": "12",
    "show_all_rows": "0",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/calendar/day';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'date' => '2025-06-21',
            'group_id' => '5',
            'resource_id' => '12',
            'show_all_rows' => '0',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/calendar/day'
params = {
  'date': '2025-06-21',
  'group_id': '5',
  'resource_id': '12',
  'show_all_rows': '0',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Day returned successfully):


{
    "date": "2025-06-21",
    "day_start": "08:00",
    "day_end": "18:00",
    "minimum_booking_duration": 30,
    "slot_minutes": 15,
    "closed": false,
    "rows_hidden": false,
    "resources": [
        {
            "id": 3,
            "name": "G-ABCD",
            "type": "resource",
            "category_id": 4,
            "category_name": "Aircraft",
            "subheading": "Cessna 172",
            "expiry_warning": null,
            "color": "#269DD9",
            "available_from": "08:00",
            "available_to": "18:00",
            "bookings": [
                {
                    "id": 120,
                    "start": "09:00",
                    "end": "11:00",
                    "color": "#269DD9",
                    "slot_text": "Jane Smith with John Doe",
                    "booked_for": "Jane Smith",
                    "booked_for_member_id": 12,
                    "reason": "Trial Flight",
                    "note": "First lesson",
                    "resource_unavailable": false,
                    "booking_overlaps_yesterday": false,
                    "booking_overlaps_tomorrow": false,
                    "im_booked": false,
                    "im_shared": false
                }
            ]
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Authenticated user is not a member of this group):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The date field is required.",
    "errors": {
        "date": [
            "The date field is required."
        ]
    }
}
 

Request   

GET v1/calendar/day

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date   string     

The calendar date to fetch (YYYY-MM-DD). Example: 2025-06-21

group_id   integer     

The group whose calendar to fetch. Example: 5

resource_id   integer  optional    

Limit the response to one resource or role-member row. Example: 12

show_all_rows   boolean  optional    

Include rows hidden by the group's "hide unavailable all day rows" setting. Example: false

Get Calendar Week

requires authentication

The website's week view: every visible resource and role member for the group with the bookings of the seven days of the week containing date. The week starts on the group's week_starts_on day (0 Sunday, 1 Monday). Built by the same code as the website (BookingModule::getWeekBookings), so hidden member details, multi-day bookings and days off behave identically. off days only apply to Pro groups.

Bookings carry hidden: true when the group hides member details and the caller may not see them (GET /booking/{id} answers 403 for those).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/calendar/week"
);

const params = {
    "date": "2025-06-21",
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/calendar/week';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'date' => '2025-06-21',
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/calendar/week'
params = {
  'date': '2025-06-21',
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Week returned successfully):


{
    "week_starts_on": 1,
    "start_date": "2025-06-16",
    "end_date": "2025-06-22",
    "days": [
        "2025-06-16",
        "2025-06-17",
        "2025-06-18",
        "2025-06-19",
        "2025-06-20",
        "2025-06-21",
        "2025-06-22"
    ],
    "categories": [
        {
            "id": 1,
            "name": "Aircraft",
            "type": "resource",
            "rows": [
                {
                    "id": 3,
                    "name": "G-ABCD",
                    "type": "resource",
                    "subheading": "Cessna 172",
                    "color": "#3366cc",
                    "expiry_warning": null,
                    "days": [
                        {
                            "date": "2025-06-16",
                            "off": false,
                            "bookings": [
                                {
                                    "id": 101,
                                    "all_day_unavailable": false,
                                    "start": "09:00",
                                    "label": "Jane Smith",
                                    "resource_unavailable": false,
                                    "hidden": false,
                                    "booking_overlaps_yesterday": false,
                                    "booking_overlaps_tomorrow": false
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Authenticated user is not a member of this group):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The date field is required.",
    "errors": {
        "date": [
            "The date field is required."
        ]
    }
}
 

Request   

GET v1/calendar/week

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date   string     

Any date in the week (YYYY-MM-DD). Example: 2025-06-21

group_id   integer     

The group whose calendar to fetch. Example: 5

Get Calendar Month

requires authentication

The website's month overview (/calendar/month): for one resource or role member, the hours booked on each day of the month, with the amber/red limits that colour them. Built by the same code as the website (BookingModule::objectBookingHours).

objects lists what can be picked (value is resource_12 or member_5). When no object is sent the group's default overview object is used, or the only object if there is just one; otherwise selected is null and days is empty. names follows the group's "hide member information" setting; when names are hidden count is the number of bookings.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/calendar/month"
);

const params = {
    "date": "2025-06-21",
    "group_id": "5",
    "object": "resource_12",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/calendar/month';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'date' => '2025-06-21',
            'group_id' => '5',
            'object' => 'resource_12',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/calendar/month'
params = {
  'date': '2025-06-21',
  'group_id': '5',
  'object': 'resource_12',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Month returned successfully):


{
    "month": "2025-06",
    "week_starts_on": 1,
    "amber_limit": 6,
    "red_limit": 10,
    "objects": [
        {
            "value": "resource_12",
            "name": "G-ABCD",
            "category_name": "Aircraft"
        }
    ],
    "selected": "resource_12",
    "subheading": "Cessna 172",
    "expiry_warning": null,
    "days": [
        {
            "date": "2025-06-01",
            "hours": 0,
            "count": 0,
            "names": [],
            "entries": [],
            "level": "not-busy"
        },
        {
            "date": "2025-06-02",
            "hours": 7.5,
            "count": 2,
            "names": [
                "Jane Smith",
                "John Doe"
            ],
            "entries": [],
            "level": "busy"
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Authenticated user is not a member of this group):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The date field is required.",
    "errors": {
        "date": [
            "The date field is required."
        ]
    }
}
 

Request   

GET v1/calendar/month

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date   string     

Any date in the month (YYYY-MM-DD). Example: 2025-06-21

group_id   integer     

The group whose calendar to fetch. Example: 5

object   string  optional    

The object to show, resource_<id> or member_<id>. Example: resource_12

iCal

Calendar Sync

requires authentication

The website's Calendar Sync page: the user's private subscription link (feed_url, to add to Google, Apple or Microsoft calendars) or has_link: false when none exists yet. masked_url is the same link with the secret hidden, for showing on screen.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/calendar-sync"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/calendar-sync';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/calendar-sync'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):


{
    "has_link": true,
    "feed_url": "https://www.shlott.com/calendar/feed/abc.ics",
    "masked_url": "https://www.shlott.com/calendar/feed/••••••••abcd.ics",
    "last_accessed_at": "2026-10-28T09:00:00+00:00",
    "last_accessed": "3 hours ago"
}
 

Request   

GET v1/user/calendar-sync

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Generate or Regenerate Calendar Link

requires authentication

Creates the link, or replaces it with a new one (the old link stops working at once).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/calendar-sync/regenerate"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/calendar-sync/regenerate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/calendar-sync/regenerate'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers)
response.json()

Example response (200):


{
    "has_link": true,
    "feed_url": "https://www.shlott.com/calendar/feed/abc.ics",
    "masked_url": "https://www.shlott.com/calendar/feed/••••••••abcd.ics",
    "last_accessed_at": null,
    "last_accessed": null,
    "message": "Your calendar link has been regenerated."
}
 

Request   

POST v1/user/calendar-sync/regenerate

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Delete Calendar Link

requires authentication

Switches calendar syncing off and removes the link.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/calendar-sync"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/calendar-sync';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/calendar-sync'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers)
response.json()

Example response (200):


{
    "has_link": false,
    "feed_url": null,
    "masked_url": null,
    "last_accessed_at": null,
    "last_accessed": null,
    "message": "Your calendar link has been deleted."
}
 

Request   

DELETE v1/user/calendar-sync

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

requires authentication

A short-lived signed URL for the booking's .ics file (the website's "add to calendar" download), for the app to open so the phone offers to add it to the user's calendar. Give end_id too for a recurring series just created (start_id / end_id from the create response); a series only covers bookings the caller created.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/booking/564/calendar-link"
);

const params = {
    "end_id": "1250",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/booking/564/calendar-link';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'end_id' => '1250',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/booking/564/calendar-link'
params = {
  'end_id': '1250',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "url": "https://example.test/signed"
}
 

Documents

List Object Documents

requires authentication

The files on the website's object page (Pro groups only). Separate from Get Object because it asks Azure storage, which can be slow, so the app loads it after the page is up.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/objects/8/documents"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/objects/8/documents';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/objects/8/documents'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Documents):


{
    "pro": true,
    "documents": [
        {
            "name": "Manual.pdf",
            "size": 120394
        }
    ]
}
 

Example response (200, Free group):


{
    "pro": false,
    "documents": []
}
 

Example response (403, Not a member):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Object not found."
}
 

Request   

GET v1/objects/{id}/documents

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The object id. Example: 8

Query Parameters

group_id   integer     

The group. Example: 5

List Group Documents

requires authentication

The group-wide files on the website's Group Documents page (Pro groups only), open to any member.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/groups/5/documents"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/groups/5/documents';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/groups/5/documents'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Documents):


{
    "pro": true,
    "documents": [
        {
            "name": "Rules.pdf",
            "size": 120394
        }
    ]
}
 

Example response (200, Free group):


{
    "pro": false,
    "documents": []
}
 

Example response (403, Not a member):


{
    "message": "Forbidden."
}
 

Request   

GET v1/groups/{id}/documents

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The group id. Example: 5

requires authentication

A short-lived signed URL for one group document. Pro groups only.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/groups/5/documents/link"
);

const params = {
    "file": "Rules.pdf",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/groups/5/documents/link';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'file' => 'Rules.pdf',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/groups/5/documents/link'
params = {
  'file': 'Rules.pdf',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "url": "https://example.test/signed"
}
 

Example response (404):


{
    "message": "Document not found."
}
 

requires authentication

A short-lived signed URL for one document, for the app to open in a browser view (the browser can't send the API key). Pro groups only.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/objects/8/documents/link"
);

const params = {
    "group_id": "5",
    "file": "Manual.pdf",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/objects/8/documents/link';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
            'file' => 'Manual.pdf',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/objects/8/documents/link'
params = {
  'group_id': '5',
  'file': 'Manual.pdf',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "url": "https://api.example.com/v1/objects/8/documents/file?...&signature=..."
}
 

Example response (403, Not a member, or not a Pro group):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Document not found."
}
 

Groups

Returns public groups whose name matches the given search term, along with their country and postcode so a new user can identify the right group before joining. The term is split on whitespace and every word must appear somewhere in the group name, in any order, so a search for "group test" also matches a group named "Test Group".

For security reasons, at most 5 groups are returned, best match first (exact name, then names starting with the term, then other matches, each alphabetical). There is no pagination, so a user must refine the search term to find a group that is not shown.

This endpoint is unauthenticated so it can be used during sign-up, before a user has an account or has joined any group.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/groups/search"
);

const params = {
    "name": "group test",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/groups/search';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'name' => 'group test',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/groups/search'
params = {
  'name': 'group test',
}
headers = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Groups found):


{
    "groups": [
        {
            "id": 5,
            "name": "Test Group",
            "country": "United Kingdom",
            "postcode": "EX1 1AA"
        },
        {
            "id": 12,
            "name": "My Test Group",
            "country": "United Kingdom",
            "postcode": "EX2 2BB"
        }
    ]
}
 

Example response (200, No groups found):


{
    "groups": []
}
 

Example response (422, Validation error):


{
    "message": "The name field is required.",
    "errors": {
        "name": [
            "The name field is required."
        ]
    }
}
 

Example response (429, Too many searches (30 a minute per IP address)):


{
    "message": "Too Many Attempts."
}
 

List Groups

requires authentication

Returns every group the authenticated user is an active member of. Use the group id values with the endpoints that take a group_id.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/groups"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/groups';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/groups'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Groups returned successfully):


{
    "groups": [
        {
            "id": 5,
            "name": "Anytown Flying Club",
            "postcode": "EX1 1AA",
            "pro": true
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Request   

GET v1/groups

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Leave Group

requires authentication

Removes the authenticated user's membership of a group. A group's only administrator cannot leave: another admin must be assigned first. If the group was the user's primary group, another group they belong to becomes primary.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/groups/5"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/groups/5';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/groups/5'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers)
response.json()

Example response (200, Left the group):


{
    "remaining_groups": 1
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (404, Not a member of the group):


{
    "message": "You are not a member of this group."
}
 

Example response (422, Only admin):


{
    "message": "Please assign another admin before leaving this group."
}
 

Request   

DELETE v1/groups/{groupId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

groupId   integer     

The group to leave. Example: 5

Manage Group Overview

requires authentication

Whether the caller can manage the group (any member may ask) and, for admins, how many objects, categories and roles it has.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Admin):


{
    "can_manage": true,
    "objects": 12,
    "categories": 3,
    "roles": 2
}
 

Example response (200, Not an admin):


{
    "can_manage": false
}
 

Example response (403, Not a member):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Delete Group: Check

requires authentication

What the app shows before the Delete Group form: the group's name and, if it still has an active Pro subscription, blocked: true with the reason (the subscription must be cancelled first, as on the website).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/delete-group"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/delete-group';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/delete-group'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "name": "Flying Club",
    "blocked": false,
    "message": null
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/delete-group

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Delete Group

requires authentication

Permanently deletes the group with its bookings, objects and members. The website's rules: administrators only, the group's name and the admin's password must be typed, a reason is required, and an active Pro subscription must be cancelled first. Cannot be undone.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/group"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "group_name": "Flying Club",
    "password": "secret123",
    "reason": "Club closed",
    "also_delete_account": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/group';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'group_name' => 'Flying Club',
            'password' => 'secret123',
            'reason' => 'Club closed',
            'also_delete_account' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/group'
payload = {
    "group_id": 5,
    "group_name": "Flying Club",
    "password": "secret123",
    "reason": "Club closed",
    "also_delete_account": true
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Group Successfully Deleted"
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Wrong name or password):


{
    "message": "Invalid group name provided.",
    "errors": {
        "group_name": [
            "Invalid group name provided."
        ]
    }
}
 

Request   

DELETE v1/manage/group

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

group_name   string     

The group's exact name. Example: Flying Club

password   string     

The admin's password. Example: secret123

reason   string     

Why the group is being deleted. Example: Club closed

also_delete_account   boolean  optional    

Also permanently delete the user's account; only honoured when this is their only group. Example: true

Members

Get Group Member

requires authentication

Returns the complete profile for a single member of the specified group. This includes their personal details (name, email, phone), postal address, emergency contact information, their permission level within the group (Read Only, Member, or Admin), any role they hold (e.g. Instructor, Examiner), and any custom fields the group administrator has defined for members.

The authenticated user must themselves be an active member of the same group to access this information. If they are not a member of that group, a 403 Forbidden response is returned. Custom fields that have been deleted by the group administrator are automatically excluded from the response.

Non-admins also get a 403 when the group hides its member list from non-admins, or when that member has chosen to hide their details from other members.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/group_member/5/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/group_member/5/12';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/group_member/5/12'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Member found):


{
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone": "01234 567890",
    "mobile": "07700 900000",
    "permission_level": "Member",
    "role": "Instructor",
    "address_1": "10 Runway Lane",
    "address_2": null,
    "address_3": null,
    "city": "Anytown",
    "state": "Exampleshire",
    "postcode": "EX1 1AA",
    "country": "United Kingdom",
    "emergency_contact_name": "Bob Smith",
    "emergency_contact_relationship": "Spouse",
    "emergency_contact_number": "07700 900001",
    "custom_fields": [
        {
            "id": "field_abc123",
            "name": "Licence Number",
            "type": "text",
            "value": "PPL/12345"
        }
    ],
    "notes": "Prefers morning slots."
}
 

Example response (401, Unauthenticated — no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Authenticated user is not a member of this group, or the profile is private):


{
    "message": "Forbidden."
}
 

Example response (404, No member with this ID exists in this group):


{
    "message": "Member not found."
}
 

Request   

GET v1/group_member/{groupId}/{memberId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

groupId   integer     

The ID of the group the member belongs to. Example: 5

memberId   integer     

The ID of the group membership record (not the user ID). Example: 12

List Members

requires authentication

One page of the group's members, searchable by name, email or role. Non-admins get a 403 when the group hides its member list from them, and never see members who hide their details. status is expired or expiring when one of the member's expiry-date custom fields is past or within 30 days; can_manage says whether the caller is a group admin.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members"
);

const params = {
    "group_id": "5",
    "search": "architecto",
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
            'search' => 'architecto',
            'page' => '1',
            'per_page' => '25',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members'
params = {
  'group_id': '5',
  'search': 'architecto',
  'page': '1',
  'per_page': '25',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Page of members):


{
    "data": [
        {
            "id": 12,
            "first_name": "Jane",
            "last_name": "Smith",
            "email": "[email protected]",
            "role": "Instructor",
            "permission_level": "Member",
            "status": null
        }
    ],
    "total": 41,
    "page": 1,
    "per_page": 25,
    "has_more": true,
    "can_manage": true,
    "removed_count": 2
}
 

Example response (403, Not a member, or the group hides its member list):


{
    "message": "Forbidden."
}
 

Request   

GET v1/members

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

search   string  optional    

Matches first name, last name, email or role. Example: architecto

page   integer  optional    

Page number, from 1. Example: 1

per_page   integer  optional    

Page size, 1 to 100 (default 25). Example: 25

Member Form Options

requires authentication

What the add and edit forms need: the group's roles, countries, the time choices for role availability, whether the group has a paid subscription (per-day availability) and whether the free member limit has been reached.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/options"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/options';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/options'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Options):


{
    "pro": true,
    "date_format": "DMY",
    "free_limit_reached": false,
    "roles": [
        {
            "id": 2,
            "name": "Instructor",
            "visible": true
        }
    ],
    "countries": [
        {
            "code": "GB",
            "name": "United Kingdom"
        }
    ],
    "times": [
        "08:00",
        "08:30"
    ]
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/members/options

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Removed Members

requires authentication

Members who left or were removed (archived), who an admin can restore or delete for good.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/removed"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/removed';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/removed'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Removed members):


{
    "data": [
        {
            "id": 9,
            "name": "Bob Jones",
            "email": "[email protected]",
            "role": null,
            "permission_level": "Member",
            "reason": "left"
        }
    ]
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/members/removed

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Expiring Members

requires authentication

Members with an expiry-date custom field that has passed or ends within 30 days.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/expiring"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/expiring';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/expiring'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Expiring members):


{
    "data": [
        {
            "id": 12,
            "name": "Jane Smith",
            "email": "[email protected]",
            "status": "expiring"
        }
    ]
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/members/expiring

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Check Email Before Adding

requires authentication

The first step of adding a member, as on the website: what happens if this email is added. status is new (the app asks for name, permission and role), existing (they already have a Shlott account, which is added as it is), removed (they were in this group before and can be restored), member (already in the group) or blacklisted.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/check-email"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "email": "[email protected]"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/check-email';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'email' => '[email protected]',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/check-email'
payload = {
    "group_id": 5,
    "email": "[email protected]"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Existing Shlott user):


{
    "status": "existing",
    "first_name": "Jane",
    "last_name": "Smith",
    "message": "..."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

POST v1/members/check-email

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

email   string     

The address to check. Example: [email protected]

Add Member

requires authentication

Adds a member to the group: someone with an existing Shlott account keeps their own details, and a new email address creates the account (the member gets a welcome email to set their password). A previous member is reinstated. Groups on the free plan are limited in size. To create a role at the same time send new_role (a name) instead of role.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "email": "[email protected]",
    "first_name": "architecto",
    "last_name": "architecto",
    "permission_level": 2,
    "role": 16,
    "new_role": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'email' => '[email protected]',
            'first_name' => 'architecto',
            'last_name' => 'architecto',
            'permission_level' => 2,
            'role' => 16,
            'new_role' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members'
payload = {
    "group_id": 5,
    "email": "[email protected]",
    "first_name": "architecto",
    "last_name": "architecto",
    "permission_level": 2,
    "role": 16,
    "new_role": "architecto"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201, Added):


{
    "message": "Member added.",
    "id": 31
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Refused):


{
    "message": "...",
    "errors": {
        "email": [
            "..."
        ]
    }
}
 

Request   

POST v1/members

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

email   string     

Email address. Example: [email protected]

first_name   string  optional    

Required for a new account, max 25. Example: architecto

last_name   string  optional    

Required for a new account, max 25. Example: architecto

permission_level   integer     

1 read only, 2 member, 3 admin. Example: 2

role   integer  optional    

An existing role id. Example: 16

new_role   string  optional    

The name of a role to create and assign. Example: architecto

Get Member For Editing

requires authentication

A member's complete editable record: details, role and permission, availability (when the role has any), address, emergency contact, the group's custom fields with their values, and notes. Group admins only; everyone else uses Get Group Member. Dates are YYYY-MM-DD and times HH:MM. A member with no email address (editable false) cannot be changed, as on the website.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
 

{
    "message": "Unauthenticated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404, No such member):


{
    "message": "Member not found."
}
 

Request   

GET v1/members/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Query Parameters

group_id   integer     

The group. Example: 5

Update Member

requires authentication

Saves the edit form. The email address cannot be changed. Sending role as null removes the role (and its availability). availability only applies when the member has a role; per-day rows are used on paid groups and everyday start/end otherwise. custom_fields is keyed by field id; checkbox fields take an array of the ticked options, dates are YYYY-MM-DD, and a field left out keeps its value.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "first_name": "architecto",
    "last_name": "architecto",
    "permission_level": 16,
    "role": 16,
    "color": "architecto",
    "note": "architecto"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'first_name' => 'architecto',
            'last_name' => 'architecto',
            'permission_level' => 16,
            'role' => 16,
            'color' => 'architecto',
            'note' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12'
payload = {
    "group_id": 5,
    "first_name": "architecto",
    "last_name": "architecto",
    "permission_level": 16,
    "role": 16,
    "color": "architecto",
    "note": "architecto"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Saved):


{
    "message": "Member details updated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404, No such member):


{
    "message": "Member not found."
}
 

Example response (422, Refused):


{
    "message": "...",
    "errors": {}
}
 

Request   

PUT v1/members/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

first_name   string     

Max 25. Example: architecto

last_name   string     

Max 25. Example: architecto

permission_level   integer     

1 read only, 2 member, 3 admin. Example: 16

role   integer  optional    

A role id, or null for none. Example: 16

color   string  optional    

Hex colour for the role, with or without #. Example: architecto

note   string  optional    

Max 380. Example: architecto

Archive Member

requires authentication

Removes a member from the group but keeps their record, so they can be restored later. Admins cannot archive themselves or the group's last admin.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12/archive';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12/archive'
payload = {
    "group_id": 5
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Archived):


{
    "message": "Member removed."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Refused):


{
    "message": "..."
}
 

Request   

POST v1/members/{id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

Restore Member

requires authentication

Brings a removed member back and sends them the email that matches their account state. Counts towards the free plan's member limit.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12/restore"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12/restore';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12/restore'
payload = {
    "group_id": 5
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Restored):


{
    "message": "Member restored."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404, No such member):


{
    "message": "Member not found."
}
 

Request   

POST v1/members/{id}/restore

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

Delete Member Impact

requires authentication

What permanently deleting a member would remove, for the confirmation screen.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12/delete-impact"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12/delete-impact';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12/delete-impact'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Impact):


{
    "name": "Bob Jones",
    "bookings": 14,
    "future_bookings": 2,
    "shared_bookings": 1,
    "attended_bookings": 0,
    "training_records": 0,
    "tickets": 0
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Cannot be deleted):


{
    "message": "..."
}
 

Request   

GET v1/members/{id}/delete-impact

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Query Parameters

group_id   integer     

The group. Example: 5

Delete Member Permanently

requires authentication

Permanently deletes a member and what goes with them (their bookings, preferences, training records and tickets). Cannot be undone, so the admin must type the member's full name and their own password, as on the website. Admins cannot delete themselves or the last admin.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "member_name": "Bob Jones",
    "password": "|]|{+-"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'member_name' => 'Bob Jones',
            'password' => '|]|{+-',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12'
payload = {
    "group_id": 5,
    "member_name": "Bob Jones",
    "password": "|]|{+-"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, json=payload)
response.json()

Example response (200, Deleted):


{
    "message": "Member deleted."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Refused):


{
    "message": "...",
    "errors": {
        "member_name": [
            "..."
        ]
    }
}
 

Request   

DELETE v1/members/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

member_name   string     

The member's full name, typed to confirm. Example: Bob Jones

password   string     

The admin's own password. Example: |]|{+-

Resend Activation Email

requires authentication

Sends the member the welcome email again (with a fresh set-password link when they have not yet chosen a password).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12/resend-activation"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12/resend-activation';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12/resend-activation'
payload = {
    "group_id": 5
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Sent):


{
    "message": "Activation email sent."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

POST v1/members/{id}/resend-activation

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

Send Password Reset Link

requires authentication

Emails the member a password reset link. Admins never set another member's password.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/members/12/password-reset"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/members/12/password-reset';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/members/12/password-reset'
payload = {
    "group_id": 5
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Sent):


{
    "message": "Password Reset Link sent. Please allow up to 30 minutes for delivery."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

POST v1/members/{id}/password-reset

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The membership id. Example: 12

Body Parameters

group_id   integer     

The group. Example: 5

Objects

List Objects

requires authentication

Every live object with its category, and when it was last booked and next booked. categories is for grouping and for the add form (it says whether a category exists at all).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/objects"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/objects';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/objects'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Objects):


{
    "data": [
        {
            "id": 8,
            "name": "Court 1",
            "category_id": 3,
            "category_name": "Courts",
            "last_booking": null,
            "next_booking": "2026-10-20 09:00:00",
            "future_bookings": 2
        }
    ],
    "free_limit_reached": false
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/objects

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Add Object

requires authentication

The new object starts with the group's opening times.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/objects"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "category_id": 3,
    "name": "Court 3",
    "subheading": "Indoor",
    "values": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/objects';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'category_id' => 3,
            'name' => 'Court 3',
            'subheading' => 'Indoor',
            'values' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/objects'
payload = {
    "group_id": 5,
    "category_id": 3,
    "name": "Court 3",
    "subheading": "Indoor",
    "values": []
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):


{
    "message": "Object successfully created.",
    "id": 21
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Invalid, or free plan limit):


{
    "message": "The name field is required.",
    "errors": {
        "name": [
            "..."
        ]
    }
}
 

Request   

POST v1/manage/objects

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

category_id   integer     

A live category of the group. Example: 3

name   string     

Example: Court 3

subheading   string  optional    

Example: Indoor

values   object  optional    

The category's properties, keyed by property id.

Update Object

requires authentication

Changing the category replaces the object's property values with the ones sent (the new category has its own fields). Opening-time availability is left untouched.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/objects/8"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "category_id": 3,
    "name": "Court 3",
    "subheading": "Indoor",
    "values": []
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/objects/8';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'category_id' => 3,
            'name' => 'Court 3',
            'subheading' => 'Indoor',
            'values' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/objects/8'
payload = {
    "group_id": 5,
    "category_id": 3,
    "name": "Court 3",
    "subheading": "Indoor",
    "values": []
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Object successfully updated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Object not found."
}
 

Request   

PUT v1/manage/objects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The object id. Example: 8

Body Parameters

group_id   integer     

The group. Example: 5

category_id   integer     

A live category of the group. Example: 3

name   string     

Example: Court 3

subheading   string  optional    

Example: Indoor

values   object  optional    

The category's properties, keyed by property id.

Delete Object

requires authentication

Hides the object from the calendar and lists; the website can restore it. Its bookings stay.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/objects/8"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/objects/8';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/objects/8'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "message": "Object successfully deleted"
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Object not found."
}
 

Request   

DELETE v1/manage/objects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The object id. Example: 8

Query Parameters

group_id   integer     

The group. Example: 5

Get Object Details

requires authentication

Used by the Day/Week object page and by the admin edit form. properties[].value is a string (YYYY-MM-DD for date / expiry_date; show it in date_format order: DMY, MDY or YMD) or, for checkbox properties, an array of the ticked options. expiry_warning is red / yellow / null and only set for expiry_date properties. availability.days is only present on Pro groups (the website only lets Pro groups set days individually).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/objects/8"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/objects/8';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/objects/8'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Object):


{
    "id": 8,
    "name": "Court 1",
    "subheading": "Indoor",
    "category_id": 3,
    "category_name": "Courts",
    "date_format": "DMY",
    "properties": [
        {
            "id": 0,
            "name": "Surface",
            "type": "text",
            "value": "Clay",
            "expiry_warning": null
        }
    ],
    "availability": {
        "start": "08:00",
        "end": "22:00",
        "pro": true,
        "days": [
            {
                "day": "monday",
                "start": "08:00",
                "end": "22:00",
                "off": false
            }
        ]
    }
}
 

Example response (403, Not a member):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Object not found."
}
 

Request   

GET v1/objects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The object id. Example: 8

Query Parameters

group_id   integer     

The group. Example: 5

Categories

List Categories

requires authentication

The group's live categories in calendar order, with their property definitions (hidden properties included, flagged deleted) and how many objects each holds.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/categories"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/categories';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/categories'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Categories):


{
    "data": [
        {
            "id": 3,
            "name": "Courts",
            "objects": 4,
            "properties": [
                {
                    "id": 0,
                    "name": "Surface",
                    "type": "select",
                    "options": "Clay,Grass",
                    "required": true,
                    "deleted": false
                }
            ]
        }
    ],
    "free_limit_reached": false,
    "date_format": "DMY"
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/categories

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Add Category

requires authentication

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/categories"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "name": "Courts",
    "properties": [
        "architecto"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/categories';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'name' => 'Courts',
            'properties' => ['architecto'],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/categories'
payload = {
    "group_id": 5,
    "name": "Courts",
    "properties": [
        "architecto"
    ]
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):


{
    "message": "Calendar Category successfully created.",
    "id": 9
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Free plan limit):


{
    "message": "Sorry! You have already exceeded free usage of Shlott. Please subscribe pro to continue ..."
}
 

Request   

POST v1/manage/categories

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

name   string     

Example: Courts

properties   string[]  optional    

Custom fields every object in the category has.

*   object  optional    
name   string     

Unique within the category. Example: Surface

type   string     

text, textarea, select, checkbox, radio, email, date or expiry_date. Example: select

options   string  optional    

Comma separated; required for select, checkbox and radio. Example: Clay,Grass

required   boolean     

Example: true

Update Category

requires authentication

Send every property the category should keep. Existing ones carry their id; new ones leave it out and are given the next free id. Set deleted to hide a property (or false to restore it). A property left out of the list is hidden.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/categories/9"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "name": "Courts",
    "properties": [
        "architecto"
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/categories/9';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'name' => 'Courts',
            'properties' => ['architecto'],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/categories/9'
payload = {
    "group_id": 5,
    "name": "Courts",
    "properties": [
        "architecto"
    ]
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Calendar Category successfully updated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Category not found."
}
 

Request   

PUT v1/manage/categories/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The category id. Example: 9

Body Parameters

group_id   integer     

The group. Example: 5

name   string     

Example: Courts

properties   string[]  optional    

The category's properties (see Add Category; plus id and deleted).

Delete Category

requires authentication

Hides the category (and so its objects) from the calendar; the website can restore it.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/categories/9"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/categories/9';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/categories/9'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "message": "Calendar Category successfully deleted."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Category not found."
}
 

Request   

DELETE v1/manage/categories/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The category id. Example: 9

Query Parameters

group_id   integer     

The group. Example: 5

Roles

List Roles

requires authentication

The group's roles in calendar order. visible roles appear as rows on the calendar; members is how many members hold the role.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/roles"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/roles';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/roles'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Roles):


{
    "data": [
        {
            "id": 2,
            "name": "Coach",
            "visible": true,
            "members": 6
        }
    ],
    "free_limit_reached": false
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Request   

GET v1/manage/roles

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. Example: 5

Add Role

requires authentication

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/roles"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "name": "Coach"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/roles';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'name' => 'Coach',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/roles'
payload = {
    "group_id": 5,
    "name": "Coach"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):


{
    "message": "Role saved.",
    "id": 7
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (422, Free plan limit):


{
    "message": "Sorry! You have already exceeded free usage of Shlott. Please subscribe pro to continue ..."
}
 

Request   

POST v1/manage/roles

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group. Example: 5

name   string     

Example: Coach

Update Role

requires authentication

Rename a role, or hide it (visible false) so it no longer has a row on the calendar.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/roles/7"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "name": "Head coach",
    "visible": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/roles/7';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'name' => 'Head coach',
            'visible' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/roles/7'
payload = {
    "group_id": 5,
    "name": "Head coach",
    "visible": true
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Role updated."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Role not found."
}
 

Request   

PUT v1/manage/roles/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The role id. Example: 7

Body Parameters

group_id   integer     

The group. Example: 5

name   string     

Example: Head coach

visible   boolean     

Example: true

Delete Role

requires authentication

The group must keep at least one role. Members keep their (now deleted) role id, as on the website.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/manage/roles/7"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/manage/roles/7';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/manage/roles/7'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, params=params)
response.json()

Example response (200):


{
    "message": "Role deleted."
}
 

Example response (403, Not a group admin):


{
    "message": "Forbidden."
}
 

Example response (404):


{
    "message": "Role not found."
}
 

Example response (422, Last role):


{
    "message": "You cannot delete the last role in the group. ..."
}
 

Request   

DELETE v1/manage/roles/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The role id. Example: 7

Query Parameters

group_id   integer     

The group. Example: 5

User

Resend Account Activation Email

Resends the activation email for an account that has a pending request to join a group but has not yet been activated. To prevent repeated requests from flooding the user's inbox, resends are limited to once every 10 minutes per pending request. The API enforces this itself: you do not need to track the cooldown, just handle the 429 response (code 6, with retry_after_seconds) if it occurs. There is also a limit of 5 requests a minute per IP address.

group_id is required alongside email because the activation email is group-specific — it names the group and is sent from that group's own contact address, and a user could in principle have more than one pending, unactivated join request across different groups.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/send-activation-email"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "[email protected]",
    "group_id": 5
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/send-activation-email';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'email' => '[email protected]',
            'group_id' => 5,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/send-activation-email'
payload = {
    "email": "[email protected]",
    "group_id": 5
}
headers = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Activation email resent):


{
    "code": 0,
    "message": "We've resent the activation email — please check your inbox."
}
 

Example response (404, No account exists for this email):


{
    "code": 2,
    "message": "No account was found for this email address."
}
 

Example response (404, Group not found):


{
    "code": 3,
    "message": "Group not found."
}
 

Example response (404, No pending join request for this email/group):


{
    "code": 4,
    "message": "No pending join request was found for this email address and group."
}
 

Example response (409, Account is already activated):


{
    "code": 5,
    "message": "This account is already activated — there is nothing to resend."
}
 

Example response (422, Validation error):


{
    "code": 1,
    "message": "The email field is required.",
    "errors": {
        "email": [
            "The email field is required."
        ]
    }
}
 

Example response (429, Resent too recently):


{
    "code": 6,
    "message": "An activation email was already sent recently. Please wait a few minutes and check your inbox before requesting another.",
    "retry_after_seconds": 480
}
 

Request   

POST v1/user/send-activation-email

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

email   string     

The email address on the pending account. Example: [email protected]

group_id   integer     

The ID of the group the pending join request is for. Example: 5

Get Profile

requires authentication

Returns the authenticated user's personal details, address and emergency contact, plus the list of countries the address can be set to. Group custom fields are not included.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/profile"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/profile';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/profile'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Profile returned):


{
    "profile": {
        "first_name": "Jane",
        "last_name": "Smith",
        "email": "[email protected]",
        "phone": "01234 567890",
        "mobile": "07700 900000",
        "address_line_1": "10 Runway Lane",
        "address_line_2": null,
        "address_line_3": null,
        "city": "Anytown",
        "county": "Exampleshire",
        "postcode": "EX1 1AA",
        "country": "GB",
        "next_of_kin": "Bob Smith",
        "next_of_kin_relationship": "Spouse",
        "next_of_kin_contact": "07700 900001"
    },
    "countries": [
        {
            "code": "GB",
            "name": "United Kingdom"
        },
        {
            "code": "US",
            "name": "United States"
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Request   

GET v1/user/profile

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update Profile

requires authentication

Updates the authenticated user's personal details, address and emergency contact. Every field is overwritten, so send the full set (blank values clear a field).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/profile"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone": "01234 567890",
    "mobile": "07700 900000",
    "address_line_1": "10 Runway Lane",
    "address_line_2": "architecto",
    "address_line_3": "architecto",
    "city": "Anytown",
    "county": "Exampleshire",
    "postcode": "EX1 1AA",
    "country": "GB",
    "next_of_kin": "Bob Smith",
    "next_of_kin_relationship": "Spouse",
    "next_of_kin_contact": "07700 900001"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/profile';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'first_name' => 'Jane',
            'last_name' => 'Smith',
            'email' => '[email protected]',
            'phone' => '01234 567890',
            'mobile' => '07700 900000',
            'address_line_1' => '10 Runway Lane',
            'address_line_2' => 'architecto',
            'address_line_3' => 'architecto',
            'city' => 'Anytown',
            'county' => 'Exampleshire',
            'postcode' => 'EX1 1AA',
            'country' => 'GB',
            'next_of_kin' => 'Bob Smith',
            'next_of_kin_relationship' => 'Spouse',
            'next_of_kin_contact' => '07700 900001',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/profile'
payload = {
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone": "01234 567890",
    "mobile": "07700 900000",
    "address_line_1": "10 Runway Lane",
    "address_line_2": "architecto",
    "address_line_3": "architecto",
    "city": "Anytown",
    "county": "Exampleshire",
    "postcode": "EX1 1AA",
    "country": "GB",
    "next_of_kin": "Bob Smith",
    "next_of_kin_relationship": "Spouse",
    "next_of_kin_contact": "07700 900001"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Profile updated):


{
    "profile": {
        "first_name": "Jane",
        "last_name": "Smith",
        "email": "[email protected]",
        "phone": "01234 567890",
        "mobile": "07700 900000",
        "address_line_1": "10 Runway Lane",
        "address_line_2": null,
        "address_line_3": null,
        "city": "Anytown",
        "county": "Exampleshire",
        "postcode": "EX1 1AA",
        "country": "GB",
        "next_of_kin": "Bob Smith",
        "next_of_kin_relationship": "Spouse",
        "next_of_kin_contact": "07700 900001"
    }
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (422, Validation error):


{
    "message": "The email has already been taken.",
    "errors": {
        "email": [
            "The email has already been taken."
        ]
    }
}
 

Request   

PUT v1/user/profile

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

first_name   string     

Example: Jane

last_name   string     

Example: Smith

email   string     

Must not be used by another account. Example: [email protected]

phone   string  optional    

Example: 01234 567890

mobile   string  optional    

Example: 07700 900000

address_line_1   string  optional    

Example: 10 Runway Lane

address_line_2   string  optional    

Example: architecto

address_line_3   string  optional    

Example: architecto

city   string  optional    

Example: Anytown

county   string  optional    

Example: Exampleshire

postcode   string  optional    

Example: EX1 1AA

country   string  optional    

A country code from the countries list returned by Get Profile. Example: GB

next_of_kin   string  optional    

Emergency contact name. Example: Bob Smith

next_of_kin_relationship   string  optional    

Example: Spouse

next_of_kin_contact   string  optional    

Emergency contact phone number. Example: 07700 900001

Change Password

requires authentication

Changes the authenticated user's login password. The current password must be supplied and the new one must be confirmed and be at least 8 characters. Limited to 6 attempts a minute.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/password"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "current_password": "oldsecret1",
    "password": "newsecret123",
    "password_confirmation": "newsecret123"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/password';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'current_password' => 'oldsecret1',
            'password' => 'newsecret123',
            'password_confirmation' => 'newsecret123',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/password'
payload = {
    "current_password": "oldsecret1",
    "password": "newsecret123",
    "password_confirmation": "newsecret123"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Password changed):


{
    "message": "Your password has been changed."
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (422, Wrong current password, or validation error):


{
    "message": "The current password is incorrect.",
    "errors": {
        "current_password": [
            "The current password is incorrect."
        ]
    }
}
 

Example response (429, Too many attempts):


{
    "message": "Too Many Attempts."
}
 

Request   

PUT v1/user/password

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

current_password   string     

The user's current password. Example: oldsecret1

password   string     

The new password (at least 8 characters). Example: newsecret123

password_confirmation   string     

Must match password. Example: newsecret123

Delete Account: What Stands in the Way

requires authentication

The groups the user belongs to (with whether anyone else administers each), their future bookings, and whether the account can be deleted now. A sole administrator of a group must first make someone else an admin or delete that group; future bookings must be cancelled first.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/delete"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/delete';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/delete'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):


{
    "groups": [
        {
            "id": 5,
            "name": "Flying Club",
            "other_admins": 0,
            "sole_admin": true
        }
    ],
    "future_bookings": [
        {
            "group_name": "Flying Club",
            "start": "2026-11-01 10:00:00",
            "end": "2026-11-01 11:00:00"
        }
    ],
    "blocked_by_groups": true,
    "blocked_by_bookings": false,
    "can_delete": false,
    "ad_free_warning": null
}
 

Request   

GET v1/user/delete

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Delete Account

requires authentication

Permanently deletes the account: personal details are wiped, the user is removed from every group, tokens are revoked and any ad-free subscription ends. Needs the account password.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "password": "secret123",
    "confirmed": true
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'password' => 'secret123',
            'confirmed' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user'
payload = {
    "password": "secret123",
    "confirmed": true
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers, json=payload)
response.json()

Example response (200):


{
    "message": "Your account has been permanently deleted."
}
 

Example response (422, Blocked or wrong password):


{
    "message": "..."
}
 

Request   

DELETE v1/user

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

password   string     

The account password. Example: secret123

confirmed   boolean     

Must be true. Example: true

Get Preferences

requires authentication

Returns the user's Shlott newsletter preference and their per-group notification preferences for every group the user is an active member of. is_admin marks groups where the daily upcoming booking list option applies.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/preferences"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/preferences';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/preferences'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Preferences returned):


{
    "receive_shlott_newsletter": true,
    "groups": [
        {
            "group_id": 5,
            "group_name": "Anytown Flying Club",
            "is_admin": false,
            "preferences": {
                "hide_details_from_other_members": false,
                "allow_members_to_send_email": true,
                "allow_members_to_change_booking": true,
                "notify_on_members_booking": false,
                "notify_on_new_member_joining": false,
                "receive_email_sent_to_whole_group": true,
                "receive_daily_upcoming_booking_list": false,
                "receive_expiry_emails": true
            }
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Request   

GET v1/user/preferences

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update Preferences

requires authentication

Updates the user's Shlott newsletter preference (receive_shlott_newsletter) and/or the preferences for one group (group_id plus any of the group preference booleans). Only the fields sent are changed. The user must be an active member of the group. Returns the full, updated preferences, as Get Preferences does.

receive_daily_upcoming_booking_list is only offered to group admins; it is ignored for other members.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/user/preferences"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "receive_shlott_newsletter": true,
    "group_id": 5,
    "preferences": {
        "hide_details_from_other_members": false,
        "allow_members_to_send_email": true,
        "allow_members_to_change_booking": true,
        "notify_on_members_booking": false,
        "notify_on_new_member_joining": false,
        "receive_email_sent_to_whole_group": true,
        "receive_daily_upcoming_booking_list": false,
        "receive_expiry_emails": true
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/user/preferences';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'receive_shlott_newsletter' => true,
            'group_id' => 5,
            'preferences' => ['hide_details_from_other_members' => false, 'allow_members_to_send_email' => true, 'allow_members_to_change_booking' => true, 'notify_on_members_booking' => false, 'notify_on_new_member_joining' => false, 'receive_email_sent_to_whole_group' => true, 'receive_daily_upcoming_booking_list' => false, 'receive_expiry_emails' => true],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/user/preferences'
payload = {
    "receive_shlott_newsletter": true,
    "group_id": 5,
    "preferences": {
        "hide_details_from_other_members": false,
        "allow_members_to_send_email": true,
        "allow_members_to_change_booking": true,
        "notify_on_members_booking": false,
        "notify_on_new_member_joining": false,
        "receive_email_sent_to_whole_group": true,
        "receive_daily_upcoming_booking_list": false,
        "receive_expiry_emails": true
    }
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Preferences updated):


{
    "receive_shlott_newsletter": true,
    "groups": [
        {
            "group_id": 5,
            "group_name": "Anytown Flying Club",
            "is_admin": false,
            "preferences": {
                "hide_details_from_other_members": false,
                "allow_members_to_send_email": true,
                "allow_members_to_change_booking": true,
                "notify_on_members_booking": false,
                "notify_on_new_member_joining": false,
                "receive_email_sent_to_whole_group": true,
                "receive_daily_upcoming_booking_list": false,
                "receive_expiry_emails": true
            }
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group):


{
    "message": "You are not a member of this group."
}
 

Example response (422, Validation error):


{
    "message": "The preferences.receive_expiry_emails field must be true or false.",
    "errors": {
        "preferences.receive_expiry_emails": [
            "The preferences.receive_expiry_emails field must be true or false."
        ]
    }
}
 

Request   

PUT v1/user/preferences

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

receive_shlott_newsletter   boolean  optional    

Whether to receive the Shlott newsletter. Example: true

group_id   integer  optional    

Required when preferences is sent: the group the preferences are for. Example: 5

preferences   object  optional    

The group preferences to change. Send only the ones that change.

hide_details_from_other_members   boolean  optional    

Hide this member's details from other members. Example: false

allow_members_to_send_email   boolean  optional    

Example: true

allow_members_to_change_booking   boolean  optional    

Example: true

notify_on_members_booking   boolean  optional    

Example: false

notify_on_new_member_joining   boolean  optional    

Example: false

receive_email_sent_to_whole_group   boolean  optional    

Example: true

receive_daily_upcoming_booking_list   boolean  optional    

Admins only. Example: false

receive_expiry_emails   boolean  optional    

Example: true

Banner

requires authentication

The site-wide announcement banner, as the user should see it in the given group: banner is null when there is no banner, it is switched off or expired, or it is targeted at other members (admins only, standard members and above) or another group type (free or Pro). version changes whenever the banner is edited. Store the version the user dismisses on the device and show the banner again when it differs. style is info, success, warning or danger. html is a safe HTML rendering (basic formatting tags and links only) and text is the same content as plain text. Render one of them.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/banner"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/banner';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/banner'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Banner showing):


{
    "banner": {
        "version": 1793352000,
        "style": "warning",
        "html": "Planned maintenance on Sunday 8am-9am.",
        "text": "Planned maintenance on Sunday 8am-9am.",
        "expires_at": "2026-11-15"
    }
}
 

Example response (200, No banner for this user):


{
    "banner": null
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The group id field is required.",
    "errors": {
        "group_id": [
            "The group id field is required."
        ]
    }
}
 

Request   

GET v1/banner

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group the user is currently in. The user must be an active member of it. Example: 5

Bookings

List Bookings

requires authentication

Returns all non-cancelled bookings that involve the authenticated user across every group they belong to. A booking is included if the user is the primary person booked, an additional member listed on the booking (e.g. an instructor or examiner), or a friend the booking has been shared with.

Results are ordered by booking start time (earliest first). Use the optional start_date and end_date query parameters to filter to a specific date window — for example, to fetch only this week's bookings or a future month's schedule. Each booking includes the resources reserved (e.g. aircraft), any additional members on the booking, and any members the booking was shared with.

Cancelled bookings are excluded, but bookings that are pending approval or on the waiting list are included and flagged with pending_approval / waiting_list. Clients should not treat those as confirmed; the website's My Bookings lists keep them out of Upcoming and Previous (a booking is confirmed when both flags are false).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/bookings"
);

const params = {
    "start_date": "2025-06-01",
    "end_date": "2025-06-30",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/bookings';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'start_date' => '2025-06-01',
            'end_date' => '2025-06-30',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/bookings'
params = {
  'start_date': '2025-06-01',
  'end_date': '2025-06-30',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Bookings returned successfully):


[
    {
        "group_id": 5,
        "group_name": "Anytown Flying Club",
        "booked_for_member_id": 12,
        "booked_for": "Jane Smith",
        "booking_start": "2025-06-15 09:00:00",
        "booking_end": "2025-06-15 11:00:00",
        "calendar_objects": [
            {
                "id": 3,
                "name": "G-ABCD"
            }
        ],
        "booked_with": [
            {
                "id": 7,
                "first_name": "John",
                "last_name": "Doe",
                "role_id": 2,
                "role_name": "Instructor"
            }
        ],
        "shared_with": [
            {
                "id": 9,
                "first_name": "Alice",
                "last_name": "Jones"
            }
        ],
        "reason": "Trial Flight",
        "notes": "Student's first lesson",
        "created_by": "Jane Smith",
        "created_at": "2025-05-20T14:32:00.000000Z",
        "pending_approval": false,
        "waiting_list": false
    }
]
 

Example response (401, Unauthenticated — no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (422, Invalid date format or end_date before start_date):


{
    "message": "The end date field must be a date after or equal to start date.",
    "errors": {
        "end_date": [
            "The end date field must be a date after or equal to start date."
        ]
    }
}
 

Request   

GET v1/bookings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

start_date   string  optional    

Only return bookings that start on or after this date (YYYY-MM-DD). Example: 2025-06-01

end_date   string  optional    

Only return bookings that start on or before this date (YYYY-MM-DD). Must be equal to or after start_date. Example: 2025-06-30

Get Booking

requires authentication

Returns a single booking by its ID. The authenticated user must be a member of the group that owns the booking. If the group has "hide member details on calendar" enabled, only admins and members directly involved in the booking (created it, booked-for, booked-with, or shared-with) may view its details. Bookings that are pending approval, on the waiting list or cancelled can only be viewed by admins and the members involved in them.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/booking/42"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/booking/42';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/booking/42'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200, Booking returned successfully):


{
    "group_id": 5,
    "group_name": "Anytown Flying Club",
    "booked_for_member_id": 12,
    "booked_for": "Jane Smith",
    "booking_start": "2025-06-15 09:00:00",
    "booking_end": "2025-06-15 11:00:00",
    "calendar_objects": [
        {
            "id": 3,
            "name": "G-ABCD"
        }
    ],
    "booked_with": [
        {
            "id": 7,
            "first_name": "John",
            "last_name": "Doe",
            "role_id": 2,
            "role_name": "Instructor"
        }
    ],
    "shared_with": [
        {
            "id": 9,
            "first_name": "Alice",
            "last_name": "Jones"
        }
    ],
    "reason": "Trial Flight",
    "notes": "Student's first lesson",
    "created_by": "Jane Smith",
    "created_at": "2025-05-20T14:32:00.000000Z"
}
 

Example response (401, Unauthenticated — no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of this group, bookings are private and user is not involved, or the booking is pending, waiting-listed or cancelled and user is not involved):


{
    "message": "Forbidden."
}
 

Example response (404, Booking not found):


{
    "message": "Not found."
}
 

Request   

GET v1/booking/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the booking to retrieve. Example: 42

Get Booking Options

requires authentication

Returns everything needed to build a booking for one group and date: what the user is allowed to do (book for others, share a booking, make recurring bookings, join the waiting list), the booking reasons, the categories with the items bookable on that date, the opening hours and the permitted start/end time steps. Groups without a paid subscription get the same shape with recurring_allowed false and the group-wide hours and availability instead of per-day ones.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/booking-options"
);

const params = {
    "group_id": "5",
    "date": "2026-10-13",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/booking-options';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
            'date' => '2026-10-13',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/booking-options'
params = {
  'group_id': '5',
  'date': '2026-10-13',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Options returned):


{
    "pro": true,
    "member_id": 12,
    "member_name": "Jane Smith",
    "permission_level": 2,
    "group_now": "2026-10-13 09:30",
    "opening_start": "08:00",
    "opening_end": "18:00",
    "minimum_booking_duration": 30,
    "minute_steps": [
        0,
        30
    ],
    "future_booking_limit_months": 6,
    "allow_past_booking": false,
    "maximum_bookable_objects": null,
    "requires_approval": false,
    "recurring_allowed": true,
    "can_book_for_others": false,
    "can_share": true,
    "members": [
        {
            "id": 7,
            "name": "John Doe"
        }
    ],
    "allow_waiting_list": true,
    "email_options": [
        {
            "value": 1,
            "label": "Do not send an email confirmation"
        },
        {
            "value": 2,
            "label": "Email only the users booked"
        }
    ],
    "default_email_option": 1,
    "reasons": [
        {
            "id": 3,
            "reason": "Trial Flight"
        }
    ],
    "categories": [
        {
            "key": "resource_4",
            "type": "resource",
            "name": "Aircraft",
            "items": [
                {
                    "id": 3,
                    "name": "G-ABCD",
                    "available_from": "08:00",
                    "available_to": "18:00"
                }
            ]
        },
        {
            "key": "role_2",
            "type": "role",
            "name": "Instructor",
            "items": [
                {
                    "id": 7,
                    "name": "John Doe",
                    "available_from": "09:00",
                    "available_to": "17:00"
                }
            ]
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group, on hold, or read-only):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The group id field is required.",
    "errors": {
        "group_id": [
            "The group id field is required."
        ]
    }
}
 

Request   

GET v1/booking-options

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group to book in. Example: 5

date   string  optional    

The day the booking is for (YYYY-MM-DD); defaults to today in the group's time zone. Example: 2026-10-13

Create Booking

requires authentication

Creates a booking (or a recurring series) for the authenticated member, applying the group's booking rules: opening hours, item availability, clashes, the waiting list, approval-only groups and the limits on how far ahead, and how many items, can be booked. At least one of resources or members is required. Returns 422 with a human-readable message when the booking is refused (clash, outside opening hours, in the past, etc). Call Get Booking Options first to find what the user can book and which options apply.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/bookings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "book_for": 77,
    "resources": [
        16
    ],
    "members": [
        16
    ],
    "shared_with": [
        16
    ],
    "start": "2026-10-13 14:00",
    "end": "2026-10-13 15:00",
    "reason": 3,
    "note": "architecto",
    "email_to": 16,
    "add_to_waiting_list_if_not_available": false,
    "recurring": "no",
    "frequency_num": 1,
    "recurring_end": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/bookings';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'book_for' => 77,
            'resources' => [16],
            'members' => [16],
            'shared_with' => [16],
            'start' => '2026-10-13 14:00',
            'end' => '2026-10-13 15:00',
            'reason' => 3,
            'note' => 'architecto',
            'email_to' => 16,
            'add_to_waiting_list_if_not_available' => false,
            'recurring' => 'no',
            'frequency_num' => 1,
            'recurring_end' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/bookings'
payload = {
    "group_id": 5,
    "book_for": 77,
    "resources": [
        16
    ],
    "members": [
        16
    ],
    "shared_with": [
        16
    ],
    "start": "2026-10-13 14:00",
    "end": "2026-10-13 15:00",
    "reason": 3,
    "note": "architecto",
    "email_to": 16,
    "add_to_waiting_list_if_not_available": false,
    "recurring": "no",
    "frequency_num": 1,
    "recurring_end": "architecto"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201, Created):


{
    "message": "Booking created.",
    "status": "success",
    "msg": "Booking created.",
    "start_id": 120,
    "end_id": 120
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group, on hold, or read-only):


{
    "message": "Forbidden."
}
 

Example response (422, Refused):


{
    "message": "Chris already booked for this time slot.",
    "errors": {
        "user_booked": [
            "Chris already booked for this time slot."
        ]
    }
}
 

Request   

POST v1/bookings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group to book in. Example: 5

book_for   integer  optional    

The group member the booking is for; defaults to the caller. Example: 77

resources   integer[]  optional    

IDs of the resources (for example aircraft or rooms) to book.

members   integer[]  optional    

IDs of group members who hold a role (for example instructors) to book alongside.

shared_with   integer[]  optional    

Members to share the booking with.

start   string     

Start, group-local time (YYYY-MM-DD HH:MM). Example: 2026-10-13 14:00

end   string     

End, group-local time (YYYY-MM-DD HH:MM). Example: 2026-10-13 15:00

reason   integer     

Booking reason id. Example: 3

note   string  optional    

Free text note. Example: architecto

email_to   integer  optional    

Email option (1 none, 2 booked members, 3 everyone, 4 admins and booked members). Example: 16

add_to_waiting_list_if_not_available   boolean  optional    

Join the waiting list rather than fail on a clash. Example: false

recurring   string  optional    

One of no, days, weeks, months (groups with a paid subscription only). Example: no

frequency_num   integer  optional    

Repeat every N units. Example: 1

recurring_end   string  optional    

Last day of the series (YYYY-MM-DD). Example: architecto

Response

Response Fields

message   string     

The result, in plain language. Use this. status and msg are the older fields, kept so existing clients keep working.

Update Booking

requires authentication

Changes a booking, applying the same booking rules as Create Booking. The body is the same as Create Booking's, without the recurring fields: a recurring series cannot be changed as a whole. Admins can change any booking in their group, other members only ones they are involved in. In a group where bookings need approval, a non-admin's change sends the booking back for approval and an admin's change approves it. Confirmation and notification emails are sent as for a new booking. Returns 422 with a human-readable message when the change is refused.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/booking/42"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "book_for": 77,
    "resources": [
        16
    ],
    "members": [
        16
    ],
    "shared_with": [
        16
    ],
    "start": "2026-10-13 14:00",
    "end": "2026-10-13 15:00",
    "reason": 3,
    "note": "architecto",
    "email_to": 16,
    "add_to_waiting_list_if_not_available": false
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/booking/42';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'book_for' => 77,
            'resources' => [16],
            'members' => [16],
            'shared_with' => [16],
            'start' => '2026-10-13 14:00',
            'end' => '2026-10-13 15:00',
            'reason' => 3,
            'note' => 'architecto',
            'email_to' => 16,
            'add_to_waiting_list_if_not_available' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/booking/42'
payload = {
    "book_for": 77,
    "resources": [
        16
    ],
    "members": [
        16
    ],
    "shared_with": [
        16
    ],
    "start": "2026-10-13 14:00",
    "end": "2026-10-13 15:00",
    "reason": 3,
    "note": "architecto",
    "email_to": 16,
    "add_to_waiting_list_if_not_available": false
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Updated):


{
    "message": "Booking updated.",
    "status": "success",
    "msg": "Booking updated.",
    "booking_id": 42
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not allowed to change it):


{
    "message": "You can only change bookings you are involved in."
}
 

Example response (404, Booking not found):


{
    "message": "Not found."
}
 

Example response (422, Refused):


{
    "message": "Chris already booked for this time slot.",
    "errors": {
        "user_booked": [
            "Chris already booked for this time slot."
        ]
    }
}
 

Request   

PUT v1/booking/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The booking to change. Example: 42

Body Parameters

book_for   integer  optional    

The group member the booking is for. Example: 77

resources   integer[]  optional    

IDs of the resources (for example aircraft or rooms) to book.

members   integer[]  optional    

IDs of group members who hold a role (for example instructors) to book alongside.

shared_with   integer[]  optional    

Members to share the booking with.

start   string     

Start, group-local time (YYYY-MM-DD HH:MM). Example: 2026-10-13 14:00

end   string     

End, group-local time (YYYY-MM-DD HH:MM). Example: 2026-10-13 15:00

reason   integer     

Booking reason id. Example: 3

note   string  optional    

Free text note. Example: architecto

email_to   integer  optional    

Email option (1 none, 2 booked members, 3 everyone, 4 admins and booked members). Example: 16

add_to_waiting_list_if_not_available   boolean  optional    

Join the waiting list rather than fail on a clash. Example: false

Response

Response Fields

message   string     

The result, in plain language. Use this. status and msg are the older fields, kept so existing clients keep working.

Cancel Booking

requires authentication

Cancels a booking. Admins can cancel any booking in their group; other members only ones they are involved in, and only when the group allows members to cancel. A cancel reason is required unless the booking is still awaiting approval. Cancelling frees the slot for the first waiting-list booking that now fits (when the group uses a waiting list) and sends the cancellation emails. The cancel_reasons and can_cancel fields of Get Booking show what a user may do.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/booking/42/cancel"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason": 2
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/booking/42/cancel';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reason' => 2,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/booking/42/cancel'
payload = {
    "reason": 2
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (200, Cancelled):


{
    "message": "Booking successfully cancelled.",
    "status": "success",
    "msg": "Booking successfully cancelled.",
    "booked_from_waiting_list": false
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not allowed to cancel):


{
    "message": "You can only cancel bookings you are involved in."
}
 

Example response (404, Booking not found):


{
    "message": "Not found."
}
 

Example response (422, Already cancelled, or no reason):


{
    "message": "Please select a reason for cancelling."
}
 

Request   

POST v1/booking/{id}/cancel

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The booking to cancel. Example: 42

Body Parameters

reason   integer  optional    

The group's booking cancel reason id (required unless the booking is pending approval). Example: 2

Response

Response Fields

message   string     

The result, in plain language. Use this. status and msg are the older fields, kept so existing clients keep working.

Noticeboard

List Notices

requires authentication

Returns the unexpired notices on a group's noticeboard, newest first. can_post says whether the user may add notices, and each notice's can_edit whether they may edit or delete it (its author, or a group admin). message is the notice exactly as it was submitted (use it to pre-fill an edit form), html is a safe HTML rendering of it (basic formatting tags and links only, with line breaks and bare links converted), and text is the same content as plain text. Render html or text, never message.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/notices"
);

const params = {
    "group_id": "5",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/notices';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'group_id' => '5',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/notices'
params = {
  'group_id': '5',
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Notices returned):


{
    "can_post": true,
    "notices": [
        {
            "id": 31,
            "subject": "Club night",
            "message": "Friday at 7pm. Map: https://example.com/map",
            "html": "Friday at 7pm. Map: <a href=\"https://example.com/map\" target=\"_blank\" rel=\"noopener noreferrer\">https://example.com/map</a>",
            "text": "Friday at 7pm. Map: https://example.com/map",
            "expire_at": "2026-11-01",
            "created_by": "Jane Smith",
            "created_at": "2026-10-10T09:15:00.000000Z",
            "can_edit": true
        }
    ]
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The group id field is required.",
    "errors": {
        "group_id": [
            "The group id field is required."
        ]
    }
}
 

Request   

GET v1/notices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

group_id   integer     

The group. The user must be an active member of it. Example: 5

Add Notice

requires authentication

Adds a notice to a group's noticeboard. Group admins can always post; other members can only post when the group allows members to post notices. Read-only members and members on hold cannot post. The message may contain basic HTML (formatting tags and links); anything else is stripped when it is displayed or emailed.

With send_to_all the notice is also emailed to every group member who has opted in to group emails. Emailing the group is limited to 3 times a day per user, and posting to 10 notices an hour.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/notices"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "group_id": 5,
    "subject": "Club night",
    "message": "Friday at 7pm.",
    "expire_at": "2026-11-01",
    "send_to_all": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/notices';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'group_id' => 5,
            'subject' => 'Club night',
            'message' => 'Friday at 7pm.',
            'expire_at' => '2026-11-01',
            'send_to_all' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/notices'
payload = {
    "group_id": 5,
    "subject": "Club night",
    "message": "Friday at 7pm.",
    "expire_at": "2026-11-01",
    "send_to_all": false
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201, Notice added):


{
    "id": 31
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group, or not allowed to post):


{
    "message": "Forbidden."
}
 

Example response (422, Validation error):


{
    "message": "The subject field must be at least 3 characters.",
    "errors": {
        "subject": [
            "The subject field must be at least 3 characters."
        ]
    }
}
 

Example response (429, Too many notices posted, or the group was emailed too often today):


{
    "message": "You have emailed the group too many times today. Please try again tomorrow."
}
 

Request   

POST v1/notices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

group_id   integer     

The group to post in. Example: 5

subject   string     

The notice title (3 to 100 characters). Example: Club night

message   string     

The notice body (at least 3 characters). Example: Friday at 7pm.

expire_at   string     

The date the notice stops being shown (YYYY-MM-DD). Example: 2026-11-01

send_to_all   boolean  optional    

Also email the notice to all members who have opted in to group emails. Example: false

Update Notice

requires authentication

Changes a notice's subject, message and expiry date. Group admins can change any notice in their group; other members only the ones they wrote (and not if they are read-only or on hold). The notice is not re-emailed.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/notices/31"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "subject": "Club night moved",
    "message": "Now on Saturday at 7pm.",
    "expire_at": "2026-11-01"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/notices/31';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'subject' => 'Club night moved',
            'message' => 'Now on Saturday at 7pm.',
            'expire_at' => '2026-11-01',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/notices/31'
payload = {
    "subject": "Club night moved",
    "message": "Now on Saturday at 7pm.",
    "expire_at": "2026-11-01"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200, Notice updated):


{
    "id": 31
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the notice's group, or not allowed to change it):


{
    "message": "Forbidden."
}
 

Example response (404, Notice not found):


{
    "message": "Not found."
}
 

Example response (422, Validation error):


{
    "message": "The message field is required.",
    "errors": {
        "message": [
            "The message field is required."
        ]
    }
}
 

Request   

PUT v1/notices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The notice to change. Example: 31

Body Parameters

subject   string     

The notice title (3 to 100 characters). Example: Club night moved

message   string     

The notice body (at least 3 characters). Example: Now on Saturday at 7pm.

expire_at   string     

The date the notice stops being shown (YYYY-MM-DD). Example: 2026-11-01

Delete Notice

requires authentication

Removes a notice from the noticeboard. Group admins can delete any notice in their group; other members only the ones they wrote (and not if they are read-only or on hold).

Example request:
const url = new URL(
    "https://api.shlott.com/v1/notices/31"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/notices/31';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/notices/31'
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('DELETE', url, headers=headers)
response.json()

Example response (200, Notice deleted):


{
    "deleted": true
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the notice's group, or not allowed to delete it):


{
    "message": "Forbidden."
}
 

Example response (404, Notice not found):


{
    "message": "Not found."
}
 

Request   

DELETE v1/notices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The notice to delete. Example: 31

Tickets

Submit Ticket

requires authentication

Raises a support ticket on behalf of the authenticated user in the given group (their primary group when group_id is omitted). The ticket appears in the group's support ticket list in Shlott, where it is handled like any other ticket. The user must be an active member of the group. The optional screen, app_version, platform, os_version and device_model fields let your application attach diagnostic detail to the ticket.

Example request:
const url = new URL(
    "https://api.shlott.com/v1/tickets"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "category": "bug",
    "message": "The calendar freezes when I rotate my phone.",
    "group_id": 5,
    "screen": "calendar",
    "app_version": "1.0.0",
    "platform": "ios",
    "os_version": "19.1",
    "device_model": "iPhone 16"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.shlott.com/v1/tickets';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'category' => 'bug',
            'message' => 'The calendar freezes when I rotate my phone.',
            'group_id' => 5,
            'screen' => 'calendar',
            'app_version' => '1.0.0',
            'platform' => 'ios',
            'os_version' => '19.1',
            'device_model' => 'iPhone 16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.shlott.com/v1/tickets'
payload = {
    "category": "bug",
    "message": "The calendar freezes when I rotate my phone.",
    "group_id": 5,
    "screen": "calendar",
    "app_version": "1.0.0",
    "platform": "ios",
    "os_version": "19.1",
    "device_model": "iPhone 16"
}
headers = {
  'Authorization': 'Bearer {YOUR_AUTH_KEY}',
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201, Ticket created):


{
    "ticket_ref": "T00123"
}
 

Example response (401, Unauthenticated: no or invalid API key):


{
    "message": "Unauthenticated."
}
 

Example response (403, Not a member of the group):


{
    "message": "You are not a member of this group."
}
 

Example response (422, Validation error):


{
    "message": "The category field must be one of: bug, suggestion, question, other.",
    "errors": {
        "category": [
            "The category field must be one of: bug, suggestion, question, other."
        ]
    }
}
 

Example response (429, Too many submissions (5 a minute)):


{
    "message": "Too Many Attempts."
}
 

Request   

POST v1/tickets

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

category   string     

One of bug, suggestion, question, other. Example: bug

message   string     

What the user wants to tell us (max 5000 characters). Example: The calendar freezes when I rotate my phone.

group_id   integer  optional    

The group the ticket relates to. Defaults to the user's primary group. Example: 5

screen   string  optional    

The screen or area of your application the ticket relates to (max 100). Example: calendar

app_version   string  optional    

The version of your application (max 50). Example: 1.0.0

platform   string  optional    

The device platform, for example ios or android (max 50). Example: ios

os_version   string  optional    

OS version (max 50). Example: 19.1

device_model   string  optional    

Device model (max 100). Example: iPhone 16