diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 4db0b6999629cbb313f14a20b38755c0103b03f0..0ebea65337913851d4730859473f16d3930aad80 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -115,7 +115,7 @@ class RouteMap
         $group->patch('/config-values/{id}', Routes\ConfigValues\ConfigValuesUpdate::class);
 
         $this->addAuthenticatedBlubberRoutes($group);
-        //        $this->addAuthenticatedConsultationRoutes($group);
+        $this->addAuthenticatedConsultationRoutes($group);
         $this->addAuthenticatedContactsRoutes($group);
         $this->addAuthenticatedCoursesRoutes($group);
         $this->addAuthenticatedCoursewareRoutes($group);
@@ -180,7 +180,7 @@ class RouteMap
 
     private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void
     {
-        $group->get('/users/{id}/consultations', Routes\Consultations\BlocksByUserIndex::class);
+        $group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class);
 
         $group->get('/consultation-blocks/{id}', Routes\Consultations\BlockShow::class);
         $group->get('/consultation-blocks/{id}/slots', Routes\Consultations\SlotsByBlockIndex::class);
@@ -189,6 +189,7 @@ class RouteMap
         $group->get('/consultation-slots/{id}/bookings', Routes\Consultations\BookingsBySlotIndex::class);
         $group->post('/consultation-slots/{id}/bookings', Routes\Consultations\BookingsCreate::class);
 
+        $group->post('/consultation-bookings', Routes\Consultations\BookingsCreate::class);
         $group->get('/consultation-bookings/{id}', Routes\Consultations\BookingsShow::class);
         $group->delete('/consultation-bookings/{id}', Routes\Consultations\BookingsDelete::class);
     }
diff --git a/lib/classes/JsonApi/Routes/Consultations/Authority.php b/lib/classes/JsonApi/Routes/Consultations/Authority.php
index 476fcbab160052e936f7db5bae5a732dcc55e007..d3022a1382067c91899d5b295a89afdf9c6e0946 100644
--- a/lib/classes/JsonApi/Routes/Consultations/Authority.php
+++ b/lib/classes/JsonApi/Routes/Consultations/Authority.php
@@ -2,55 +2,57 @@
 
 namespace JsonApi\Routes\Consultations;
 
-use ConsultationBlock;
-use ConsultationBooking;
-use ConsultationSlot;
+use JsonApi\Schemas\ConsultationBooking;
 
-class Authority
+final class Authority
 {
-    // TODO
-    public static function canShowBlubberThread(User $user, BlubberThread $resource)
+    public static function canShowRange(\User $user, \Range $range): bool
     {
-        return self::userIsAuthor($user) && $resource->isReadable($user->id);
+        return $range->isAccessibleToUser($user->id);
     }
 
-    public static function canCreatePrivateBlubberThread(User $user)
+    public static function canEditRange(\User $user, \Range $range): bool
     {
-        return self::userIsAuthor($user);
+        return $range->isEditableByUser($user->id);
     }
 
-    public static function canCreateComment(User $user, BlubberThread $resource)
+    public static function canShowBlock(\User $user, \ConsultationBlock $block): bool
     {
-        return self::userIsAuthor($user) && $resource->isCommentable($user->id);
+        return self::canShowRange($user, $block->range);
     }
 
-    public static function canDeleteComment(User $user, BlubberComment $resource)
+    public static function canEditBlock(\User $user, \ConsultationBlock $block): bool
     {
-        return self::canEditComment($user, $resource);
+        return self::canEditRange($user, $block->range);
     }
 
-    public static function canEditComment(User $user, BlubberComment $resource)
+    public static function canShowSlot(\User $user, \ConsultationSlot $slot): bool
     {
-        return self::userIsAuthor($user) && $resource->isWritable($user->id);
+        return self::canShowBlock($user, $slot->block);
     }
 
-    public static function canIndexComments(User $user, BlubberThread $resource = null)
+    public static function canEditSlot(\User $user, \ConsultationSlot $slot): bool
     {
-        return isset($resource)
-            ? self::canShowBlubberThread($user, $resource)
-            : self::userIsAuthor($user);
+        return self::canEditBlock($user, $slot->block);
     }
 
-    public static function canShowComment(User $user, BlubberComment $resource)
+    public static function canBookSlot(\User $user, \ConsultationSlot $slot): bool
     {
-        return self::canShowBlubberThread($user, $resource->thread);
+        return \ConsultationBooking::userMayCreateBookingForRange(
+            $slot->block->range,
+            $user
+        );
     }
 
-    /**
-     * @SuppressWarnings(PHPMD.Superglobals)
-     */
-    private static function userIsAuthor(User $user)
+    public static function canShowBooking(\User $user, \ConsultationBooking $booking): bool
     {
-        return $GLOBALS['perm']->have_perm('autor', $user->id);
+        return self::canShowSlot($user, $booking->slot)
+            || $booking->user_id === $user->id;
+    }
+
+    public static function canEditBooking(\User $user, \ConsultationBooking $booking): bool
+    {
+        return self::canEditSlot($user, $booking->slot)
+            || $booking->user_id === $user->id;
     }
 }
diff --git a/lib/classes/JsonApi/Routes/Consultations/BlockShow.php b/lib/classes/JsonApi/Routes/Consultations/BlockShow.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..09baea730ba9003016cb17791906a747357f1f9f 100644
--- a/lib/classes/JsonApi/Routes/Consultations/BlockShow.php
+++ b/lib/classes/JsonApi/Routes/Consultations/BlockShow.php
@@ -0,0 +1,31 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\ConsultationBlock;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class BlockShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        ConsultationBlock::REL_SLOTS,
+        ConsultationBlock::REL_RANGE,
+    ];
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $block = \ConsultationBlock::find($args['id']);
+        if (!$block) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowBlock($this->getUser($request), $block)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($block);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php b/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..41474b763c5b79864fba2b766322c24525cdde56
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php
@@ -0,0 +1,65 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\ConsultationBlock;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays all consultation blocks of a range
+ */
+class BlocksByRangeIndex extends JsonApiController
+{
+    use FilterTrait;
+
+    protected $allowedIncludePaths = [
+        ConsultationBlock::REL_SLOTS,
+        ConsultationBlock::REL_RANGE,
+    ];
+    protected $allowedPagingParameters = ['offset', 'limit'];
+    protected $allowedFilteringParameters = ['current', 'expired'];
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $this->validateFilters();
+
+        $range_id = $args['id'];
+        $range_type = substr($args['type'], 0, -1); // Strips trailing plural s
+
+        $range = \RangeFactory::createRange($range_type, $range_id);
+        if ($range->isNew()) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowRange($this->getUser($request), $range)) {
+            throw new AuthorizationFailedException();
+        }
+
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        $filters = $this->getFilters();
+        $blocks = $this->getBlocks($range, $filters);
+
+        return $this->getPaginatedContentResponse(
+            $blocks->limit($offset, $limit)->getArrayCopy(),
+            count($blocks)
+        );
+    }
+
+    private function getBlocks(\Range $range, array $filters): \SimpleCollection
+    {
+        if (!$filters['current'] && !$filters['expired']) {
+            return \SimpleCollection::createFromArray([]);
+        }
+
+        if ($filters['current'] && $filters['expired']) {
+            return $range->consultation_blocks;
+        }
+
+        $blocks = \ConsultationBlock::findByRange($range, 'ORDER BY start', $filters['expired']);
+        return \SimpleCollection::createFromArray($blocks);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php b/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php
deleted file mode 100644
index 0705ff275fca56720791d6812fcf85e3edf433cc..0000000000000000000000000000000000000000
--- a/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-namespace JsonApi\Routes\Consultations;
-
-use JsonApi\Errors\AuthorizationFailedException;
-use JsonApi\Errors\RecordNotFoundException;
-use JsonApi\JsonApiController;
-use JsonApi\Routes\TimestampTrait;
-use Psr\Http\Message\ResponseInterface as Response;
-use Psr\Http\Message\ServerRequestInterface as Request;
-
-/**
- * Displays all consultation blocks of a user
- */
-class BlocksByUserIndex extends JsonApiController
-{
-    use TimestampTrait, FilterTrait;
-
-    protected $allowedFilteringParameters = ['since', 'before'];
-//    protected $allowedIncludePaths = ['author', 'mentions', 'thread'];
-    protected $allowedPagingParameters = ['offset', 'limit'];
-
-    /**
-     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     */
-    public function __invoke(Request $request, Response $response, $args)
-    {
-        $this->validateFilters();
-
-        if (!($user = \User::find($args['id']))) {
-            throw new RecordNotFoundException();
-        }
-
-        if (!Authority::canShowBlubberThread($this->getUser($request), $thread)) {
-            throw new AuthorizationFailedException();
-        }
-
-        $filters = $this->getFilters();
-        list($total, $comments) = $this->getComments($thread, $filters);
-
-        return $this->getPaginatedContentResponse($comments, $total);
-    }
-
-    private function getComments(\BlubberThread $thread, array $filters)
-    {
-        list($offset, $limit) = $this->getOffsetAndLimit();
-
-        $query = 'thread_id = :thread_id';
-        $params = ['thread_id' => $thread->id];
-
-        if (isset($filters['before'])) {
-            $query .= ' AND mkdate <= :before';
-            $params['before'] = $filters['before'];
-        }
-
-        if (isset($filters['since'])) {
-            $query .= ' AND mkdate >= :since';
-            $params['since'] = $filters['since'];
-        }
-
-        if (isset($filters['search'])) {
-            $query .= ' AND content LIKE :search';
-            $params['search'] = '%' . $filters['search'] . '%';
-        }
-
-        $query .= ' ORDER BY mkdate ASC LIMIT :limit OFFSET :offset';
-        $params['limit'] = $limit + 1;
-        $params['offset'] = $offset;
-
-        $comments = \BlubberComment::findBySQL($query, $params);
-        return [count($comments) <= $limit ? count($comments) + $offset : null, array_slice($comments, 0, $limit)];
-    }
-}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php b/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0bb846b1cf4d983352115b2e334af7b579593915 100644
--- a/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php
+++ b/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php
@@ -0,0 +1,25 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class BookingsBySlotIndex extends JsonApiController
+{
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $slot = \ConsultationSlot::find($args['id']);
+        if (!$slot) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowSlot($this->getUser($request), $slot)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($slot->bookings);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php b/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a7347ab43b4aa9e54aa928971bc918738a4e12e1 100644
--- a/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php
+++ b/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php
@@ -0,0 +1,98 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\ConflictException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\ConsultationSlot;
+use JsonApi\Schemas\User;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class BookingsCreate extends JsonApiController
+{
+    use ValidationTrait;
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request, $args);
+
+        $slot = $this->getBookingSlot($json, $args);
+        $booking_user = $this->getBookingUser($json);
+
+        if (!Authority::canBookSlot($booking_user, $slot)) {
+            throw new AuthorizationFailedException();
+        }
+
+        if ($slot->isOccupied()) {
+            throw new ConflictException('The slot is already occupied');
+        }
+
+        $booking = \ConsultationBooking::create([
+            'slot_id' => $slot->id,
+            'user_id' => $booking_user->id,
+            'reason'  => self::arrayGet($json, 'data.attributes.reason', ''),
+        ]);
+
+        return $this->getCreatedResponse($booking);
+    }
+
+    protected function validateResourceDocument($json, $data)
+    {
+        $user_validation_error = $this->validateRequestContainsValidUser($json, $data);
+        $slot_validation_error = $this->validateRequestContainsValidSlot($json, $data);
+
+        return $user_validation_error ?? $slot_validation_error;
+    }
+
+    protected function validateRequestContainsValidUser($json, $data)
+    {
+        if (!self::arrayHas($json, 'data.relationships.user')) {
+            return 'No user relationship defined for booking';
+        }
+
+        $booking_user = self::arrayGet($json, 'data.relationships.user');
+        if (!isset($booking_user['data']['type'], $booking_user['data']['id']) || $booking_user['data']['type'] !== User::TYPE) {
+            return 'Defined booking user has invalid format.';
+        }
+        if (!\User::exists($booking_user['data']['id'])) {
+            return 'Defined booking user does not exist.';
+        }
+
+        return null;
+    }
+
+    protected function validateRequestContainsValidSlot($json, $data)
+    {
+        if (isset($data['id']) && \ConsultationSlot::exists($data['id'])) {
+            return null;
+        }
+
+        if (!self::arrayHas($json, 'data.relationships.slot')) {
+            return 'No slot relationship defined for booking';
+        }
+
+        $booking_slot = self::arrayGet($json, 'data.relationships.slot');
+        if (!isset($booking_slot['data']['type'], $booking_slot['data']['id']) || $booking_slot['data']['type'] !== ConsultationSlot::TYPE) {
+            return 'Defined slot for booking has invalid format.';
+        }
+        if (!\ConsultationSlot::exists($booking_slot['data']['id'])) {
+            return 'Defined slot for booking does not exist.';
+        }
+
+        return null;
+    }
+
+    protected function getBookingUser($json): \User
+    {
+        $user_id = self::arrayGet($json, 'data.relationships.user.data.id');
+        return \User::find($user_id);
+    }
+
+    protected function getBookingSlot($json, $data): \ConsultationSlot
+    {
+        $slot_id = self::arrayGet($json, 'data.relationships.slot.data.id', $data['id'] ?? null);
+        return \ConsultationSlot::find($slot_id);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..dadf3a3c90ca0a258ae26ac4e49516e0103fc5d9 100644
--- a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php
+++ b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php
@@ -0,0 +1,39 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class BookingsDelete extends JsonApiController
+{
+    use ValidationTrait;
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+
+        $booking = \ConsultationBooking::find($args['id']);
+        if (!$booking) {
+            throw new RecordNotFoundException();
+        }
+
+        $user = $this->getUser($request);
+        if (!Authority::canEditBooking($user, $booking)) {
+            throw new AuthorizationFailedException();
+        }
+
+        $reason = self::arrayGet($json, 'data.attributes.reason', '');
+
+        $booking->cancel($reason);
+
+        return $this->getCodeResponse(204);
+    }
+
+    protected function validateResourceDocument($json, $data)
+    {
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php b/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..31df647ecad1fe6f606bd0658f945558db7e89e0 100644
--- a/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php
+++ b/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php
@@ -0,0 +1,31 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\ConsultationBooking;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class BookingsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        ConsultationBooking::REL_SLOT,
+        ConsultationBooking::REL_USER,
+    ];
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $booking = \ConsultationBooking::find($args['id']);
+        if (!$booking) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowBooking($this->getUser($request), $booking)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($booking);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php b/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..8aed18ef1ce10804b89b3508f1476dd995f40bbf
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php
@@ -0,0 +1,36 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\BadRequestException;
+
+trait FilterTrait
+{
+    private function validateFilters()
+    {
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?? [];
+
+        if (array_key_exists('current', $filtering)) {
+            if (!ctype_digit($filtering['current']) || !in_array($filtering['current'], [0, 1])) {
+                throw new BadRequestException('Filter "current" may only be 0 or 1.');
+            }
+        }
+
+        if (array_key_exists('expired', $filtering)) {
+            if (!ctype_digit($filtering['expired']) || !in_array($filtering['expired'], [0, 1])) {
+                throw new BadRequestException('Filter "expired" may only be 0 or 1.');
+            }
+        }
+    }
+
+    private function getFilters(): array
+    {
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?? [];
+
+        $has_filter = isset($filtering['current']) || isset($filtering['expired']);
+
+        $filters['current'] = $filtering['current'] ?? !$has_filter;
+        $filters['expired'] = $filtering['expired'] ?? !$has_filter;
+
+        return $filters;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotShow.php b/lib/classes/JsonApi/Routes/Consultations/SlotShow.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1de10cf01deeb8f2ae772ce24041f7b82b74aac6 100644
--- a/lib/classes/JsonApi/Routes/Consultations/SlotShow.php
+++ b/lib/classes/JsonApi/Routes/Consultations/SlotShow.php
@@ -0,0 +1,31 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\ConsultationSlot;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class SlotShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        ConsultationSlot::REL_BLOCK,
+        ConsultationSlot::REL_BOOKINGS,
+    ];
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $slot = \ConsultationSlot::find($args['id']);
+        if (!$slot) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowSlot($this->getUser($request), $slot)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($slot);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php b/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7953f4517f374f06449bd767cb1dfe3b74ee20b8 100644
--- a/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php
+++ b/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php
@@ -0,0 +1,37 @@
+<?php
+namespace JsonApi\Routes\Consultations;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\ConsultationSlot;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class SlotsByBlockIndex extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        ConsultationSlot::REL_BLOCK,
+        ConsultationSlot::REL_BOOKINGS,
+    ];
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $block = \ConsultationBlock::find($args['id']);
+        if (!$block) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowBlock($this->getUser($request), $block)) {
+            throw new AuthorizationFailedException();
+        }
+
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            $block->slots->limit($offset, $limit),
+            count($block->slots)
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index de6e24759ea86270ec415fc886734fe7ae204571..1af7a90b38092635403f3a03f0369561cf0a9d4e 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -19,6 +19,9 @@ class SchemaMap
             \BlubberThread::class => Schemas\BlubberThread::class,
 
             \CalendarEvent::class => Schemas\CalendarEvent::class,
+            \ConsultationBlock::class => Schemas\ConsultationBlock::class,
+            \ConsultationBooking::class => Schemas\ConsultationBooking::class,
+            \ConsultationSlot::class => Schemas\ConsultationSlot::class,
             \ConfigValue::class => Schemas\ConfigValue::class,
             \CourseEvent::class => Schemas\CourseEvent::class,
             \ContentTermsOfUse::class => Schemas\ContentTermsOfUse::class,
diff --git a/lib/classes/JsonApi/Schemas/ConsultationBlock.php b/lib/classes/JsonApi/Schemas/ConsultationBlock.php
index 83ea3db3bd9504c126d5580baa484949dd690328..4e4e8d72e1258eb84d129a92a932c31822db61e0 100644
--- a/lib/classes/JsonApi/Schemas/ConsultationBlock.php
+++ b/lib/classes/JsonApi/Schemas/ConsultationBlock.php
@@ -8,9 +8,13 @@ use Neomerx\JsonApi\Schema\Link;
 class ConsultationBlock extends SchemaProvider
 {
     const TYPE = 'consultation-blocks';
+
     const REL_SLOTS = 'slots';
     const REL_RANGE = 'range';
 
+    /**
+     * @param \ConsultationBlock $resource
+     */
     public function getId($resource): ?string
     {
         return $resource->id;
@@ -22,15 +26,16 @@ class ConsultationBlock extends SchemaProvider
             'start' => date('c', $resource->start),
             'end'   => date('c', $resource->end),
 
-            'room' => $resource->room,
-            'size' => (int) $resource->size,
-
+            'size'              => (int) $resource->size,
             'show-participants' => (bool) $resource->show_participants,
             'require-reason'    => $resource->require_reason,
 
             'confirmation-text'      => $resource->confirmation_text ?: null,
             'confirmation-text-html' => formatLinks($resource->confirmation_text) ?: null,
 
+            'room'      => $resource->room,
+            'room-html' => formatLinks($resource->room),
+
             'note'      => $resource->note,
             'note-html' => formatLinks($resource->note),
 
@@ -49,28 +54,27 @@ class ConsultationBlock extends SchemaProvider
     public function getRelationships($resource, ContextInterface $context): iterable
     {
         $relationships = [];
-        $relationships = $this->getSlotsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOTS));
 
         $isPrimary = $context->getPosition()->getLevel() === 0;
-        if (!$isPrimary) {
-            return $relationships;
+        if ($isPrimary) {
+            $relationships = $this->getSlotsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOTS));
+            $relationships = $this->getRangeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_RANGE));
         }
 
-        $relationships = $this->getRangeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_RANGE));
 
         return $relationships;
     }
 
     // #### PRIVATE HELPERS ####
 
-    private function getSlotsRelationship(array $relationships, \BlubberComment $resource, $includeData)
+    private function getSlotsRelationship(array $relationships, \ConsultationBlock $resource, $includeData)
     {
         if ($includeData) {
             $relatedSlots = $resource->slots;
         } else {
-            $relatedSlots = array_map(function ($slot) {
+            $relatedSlots = $resource->slots->map(function ($slot) {
                 return \ConsultationSlot::build(['id' => $slot->id], false);
-            }, $resource->slots);
+            });
         }
 
         $relationships[self::REL_SLOTS] = [
@@ -97,7 +101,7 @@ class ConsultationBlock extends SchemaProvider
         return $relationships;
     }
 
-    private function getLinkForRange(Range $range)
+    private function getLinkForRange(\Range $range)
     {
         if (
             $range instanceof \Course ||
@@ -110,18 +114,18 @@ class ConsultationBlock extends SchemaProvider
         throw new \Exception('Unknown range type');
     }
 
-    private function getMinimalRange(Range $range)
+    private function getMinimalRange(\Range $range)
     {
         if ($range instanceof \Course) {
-            return Course::build(['id' => $range->id], false);
+            return \Course::build(['id' => $range->id], false);
         }
 
         if ($range instanceof \Institute) {
-            return Institute::build(['id' => $range->id], false);
+            return \Institute::build(['id' => $range->id], false);
         }
 
         if ($range instanceof \User) {
-            return User::build(['id' => $range->id], false);
+            return \User::build(['id' => $range->id], false);
         }
 
         throw new \Exception('Unknown range type');
diff --git a/lib/classes/JsonApi/Schemas/ConsultationBooking.php b/lib/classes/JsonApi/Schemas/ConsultationBooking.php
index 0368ac566edba7a79e4ee838107b6429ddeb7797..5aa96310c96ca838b6039576b3aecad284ef2927 100644
--- a/lib/classes/JsonApi/Schemas/ConsultationBooking.php
+++ b/lib/classes/JsonApi/Schemas/ConsultationBooking.php
@@ -8,14 +8,21 @@ use Neomerx\JsonApi\Schema\Link;
 class ConsultationBooking extends SchemaProvider
 {
     const TYPE = 'consultation-bookings';
+
     const REL_SLOT = 'slot';
     const REL_USER = 'user';
 
+    /**
+     * @param \ConsultationBooking $resource
+     */
     public function getId($resource): ?string
     {
         return $resource->id;
     }
 
+    /**
+     * @param \ConsultationBooking $resource
+     */
     public function getAttributes($resource, ContextInterface $context): iterable
     {
         $attributes = [
@@ -28,23 +35,22 @@ class ConsultationBooking extends SchemaProvider
         return $attributes;
     }
 
-    /**
-     * In dieser Methode können Relationships zu anderen Objekten
-     * spezifiziert werden.
-     * {@inheritdoc}
-     */
     public function getRelationships($resource, ContextInterface $context): iterable
     {
         $relationships = [];
-        $relationships = $this->getSlotRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOT));
-        $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER));
+
+        $isPrimary = $context->getPosition()->getLevel() === 0;
+        if ($isPrimary) {
+            $relationships = $this->getSlotRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOT));
+            $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER));
+        }
 
         return $relationships;
     }
 
     // #### PRIVATE HELPERS ####
 
-    private function getSlotRelationship(array $relationships, \BlubberComment $resource, $includeData)
+    private function getSlotRelationship(array $relationships, \ConsultationBooking $resource, $includeData)
     {
         $slot = $resource->slot;
 
@@ -58,7 +64,7 @@ class ConsultationBooking extends SchemaProvider
         return $relationships;
     }
 
-    private function getRangeRelationship($relationships, $resource, $includeData)
+    private function getUserRelationship($relationships, \ConsultationBooking $resource, $includeData)
     {
         $user = $resource->user;
 
diff --git a/lib/classes/JsonApi/Schemas/ConsultationSlot.php b/lib/classes/JsonApi/Schemas/ConsultationSlot.php
index a630bb6a42f98e8665d944b6d1debf2816003c22..92678f553224ab8acc3a49f010361c748d0a4620 100644
--- a/lib/classes/JsonApi/Schemas/ConsultationSlot.php
+++ b/lib/classes/JsonApi/Schemas/ConsultationSlot.php
@@ -8,11 +8,10 @@ use Neomerx\JsonApi\Schema\Link;
 class ConsultationSlot extends SchemaProvider
 {
     const TYPE = 'consultation-slots';
+
     const REL_BLOCK = 'block';
     const REL_BOOKINGS = 'bookings';
 
-
-
     public function getId($resource): ?string
     {
         return $resource->id;
@@ -42,21 +41,20 @@ class ConsultationSlot extends SchemaProvider
     public function getRelationships($resource, ContextInterface $context): iterable
     {
         $relationships = [];
-        $relationships = $this->getBlockRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BLOCK));
 
         $isPrimary = $context->getPosition()->getLevel() === 0;
-        if (!$isPrimary) {
-            return $relationships;
+        if ($isPrimary) {
+            $relationships = $this->getBlockRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BLOCK));
+            $relationships = $this->getBookingsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BOOKINGS));
         }
 
-        $relationships = $this->getBookingsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BOOKINGS));
 
         return $relationships;
     }
 
     // #### PRIVATE HELPERS ####
 
-    private function getBlockRelationship($relationships, $resource, $includeData)
+    private function getBlockRelationship($relationships, \ConsultationSlot $resource, $includeData)
     {
         $block = $resource->block;
 
@@ -70,14 +68,14 @@ class ConsultationSlot extends SchemaProvider
         return $relationships;
     }
 
-    private function getBookingsRelationship(array $relationships, \BlubberComment $resource, $includeData)
+    private function getBookingsRelationship(array $relationships, \ConsultationSlot $resource, $includeData)
     {
         if ($includeData) {
             $relatedBookings = $resource->bookings;
         } else {
-            $relatedBookings = array_map(function ($booking) {
-                return \ConsultationBooking::build(['slot_id' => $booking->slot_id], false);
-            }, $resource->bookings);
+            $relatedBookings = $resource->bookings->map(function ($booking) {
+                return \ConsultationBooking::build(['booking_id' => $booking->id], false);
+            });
         }
 
         $relationships[self::REL_BOOKINGS] = [
diff --git a/lib/classes/RangeFactory.php b/lib/classes/RangeFactory.php
index 73cf11e9b7bd0130acdc775509760dfb7e14a4c7..8da592a28a18e8311d688440d4181d0eb8b217a0 100644
--- a/lib/classes/RangeFactory.php
+++ b/lib/classes/RangeFactory.php
@@ -31,7 +31,7 @@ final class RangeFactory
      * @param string $type Range type
      * @param mixed  $id   Range id
      * @return mixed any of the supported range types
-     * @throws Exception when an invalid range type was given
+     * @throws Exception when an invalid range type was given
      *
      * @todo Should this be more dynamic in case any more ranges are added?
      */
diff --git a/lib/models/ConsultationBooking.php b/lib/models/ConsultationBooking.php
index 3e02ba014ce308c870938ab00926b9e6ab480940..1903249d6a2710bc65f56980758737ccb69d5b8b 100644
--- a/lib/models/ConsultationBooking.php
+++ b/lib/models/ConsultationBooking.php
@@ -94,6 +94,26 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject
         parent::configure($config);
     }
 
+    /**
+     * Returns whether a user may create a booking for the given range.
+     *
+     * @param User $user
+     * @return bool
+     */
+    public static function userMayCreateBookingForRange(\Range $range, \User $user): bool
+    {
+        if (!($range instanceof \User)) {
+            return true;
+        }
+
+        $allowed_perms = ['user', 'autor', 'tutor'];
+        if (Config::get()->CONSULTATION_ALLOW_DOCENTS_RESERVING) {
+            $allowed_perms[] = 'dozent';
+        }
+
+        return in_array($user->perms, $allowed_perms);
+    }
+
     public function cancel($reason = '')
     {
         if ($GLOBALS['user']->id !== $this->user_id) {
diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php
index ba36ee9fb901c2ee8469ff142e4d9866e2c0e141..10969a4a877962a8fe490d9e025051064c3b25fa 100644
--- a/lib/models/Course.class.php
+++ b/lib/models/Course.class.php
@@ -142,6 +142,11 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
             'on_delete'  => 'delete',
             'on_store'   => 'store',
         ];
+        $config['has_many']['consultation_blocks'] = [
+            'class_name'        => ConsultationBlock::class,
+            'assoc_foreign_key' => 'range_id',
+            'on_delete'         => 'delete',
+        ];
 
         $config['has_and_belongs_to_many']['semesters'] = [
             'class_name'     => Semester::class,
diff --git a/lib/models/Institute.class.php b/lib/models/Institute.class.php
index 435fd98810359edd9abceb29dda067c43a382fa7..b25e3597e42be6a4224c063a3b1c57797fb01be4 100644
--- a/lib/models/Institute.class.php
+++ b/lib/models/Institute.class.php
@@ -107,6 +107,11 @@ class Institute extends SimpleORMap implements Range
             'on_delete'  => 'delete',
             'on_store'   => 'store',
         ];
+        $config['has_many']['consultation_blocks'] = [
+            'class_name'        => ConsultationBlock::class,
+            'assoc_foreign_key' => 'range_id',
+            'on_delete'         => 'delete',
+        ];
         $config['has_many']['tools'] = [
             'class_name'        => ToolActivation::class,
             'assoc_foreign_key' => 'range_id',
diff --git a/lib/navigation/ConsultationNavigation.php b/lib/navigation/ConsultationNavigation.php
index 73aaf5497f597ea816be8b5a6e90c82e60eb975e..e68982e3b5f14f00a9c1f662cc874ec92f085a5c 100644
--- a/lib/navigation/ConsultationNavigation.php
+++ b/lib/navigation/ConsultationNavigation.php
@@ -55,17 +55,8 @@ class ConsultationNavigation extends Navigation
             return;
         }
 
-        if ($this->range instanceof User) {
-            // Permissions that are allowed to book reservervations
-            $allowed = ['user', 'autor', 'tutor'];
-            if (Config::get()->CONSULTATION_ALLOW_DOCENTS_RESERVING) {
-                $allowed[] = 'dozent';
-            }
-
-            // User does not have required permissions
-            if (!in_array($GLOBALS['user']->perms, $allowed)) {
-                return null;
-            }
+        if (!ConsultationBooking::userMayCreateBookingForRange($this->range, User::findCurrent())) {
+            return;
         }
 
         // Create visitor navigation