diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php
index d130304de..4cba7900b 100644
--- a/app/Api/ApiDocsGenerator.php
+++ b/app/Api/ApiDocsGenerator.php
@@ -55,10 +55,16 @@ class ApiDocsGenerator
     {
         return $routes->map(function (array $route) {
             $exampleTypes = ['request', 'response'];
+            $fileTypes = ['json', 'http'];
             foreach ($exampleTypes as $exampleType) {
-                $exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}.json");
-                $exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
-                $route["example_{$exampleType}"] = $exampleContent;
+                foreach ($fileTypes as $fileType) {
+                    $exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}." . $fileType);
+                    if (file_exists($exampleFile)) {
+                        $route["example_{$exampleType}"] = file_get_contents($exampleFile);
+                        continue 2;
+                    }
+                }
+                $route["example_{$exampleType}"] = null;
             }
 
             return $route;
diff --git a/app/Entities/Tools/SearchRunner.php b/app/Entities/Tools/SearchRunner.php
index f6da871f4..04f4c5768 100644
--- a/app/Entities/Tools/SearchRunner.php
+++ b/app/Entities/Tools/SearchRunner.php
@@ -56,6 +56,8 @@ class SearchRunner
      * Search all entities in the system.
      * The provided count is for each entity to search,
      * Total returned could be larger and not guaranteed.
+     *
+     * @return array{total: int, count: int, has_more: bool, results: Entity[]}
      */
     public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
     {
diff --git a/app/Http/Controllers/Api/SearchApiController.php b/app/Http/Controllers/Api/SearchApiController.php
new file mode 100644
index 000000000..8fb249665
--- /dev/null
+++ b/app/Http/Controllers/Api/SearchApiController.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\SearchOptions;
+use BookStack\Entities\Tools\SearchRunner;
+use Illuminate\Http\Request;
+
+class SearchApiController extends ApiController
+{
+    protected $searchRunner;
+
+    protected $rules = [
+        'all' => [
+            'query' => ['required'],
+            'page'  => ['integer', 'min:1'],
+            'count'  => ['integer', 'min:1', 'max:100'],
+        ],
+    ];
+
+    public function __construct(SearchRunner $searchRunner)
+    {
+        $this->searchRunner = $searchRunner;
+    }
+
+    /**
+     * Run a search query against all main content types (shelves, books, chapters & pages)
+     * in the system. Takes the same input as the main search bar within the BookStack
+     * interface as a 'query' parameter. See https://www.bookstackapp.com/docs/user/searching/
+     * for a full list of search term options. Results contain a 'type' property to distinguish
+     * between: bookshelf, book, chapter & page.
+     *
+     * The paging parameters and response format emulates a standard listing endpoint
+     * but standard sorting and filtering cannot be done on this endpoint. If a count value
+     * is provided this will only be taken as a suggestion. The results in the response
+     * may currently be up to 4x this value.
+     */
+    public function all(Request $request)
+    {
+        $this->validate($request, $this->rules['all']);
+
+        $options = SearchOptions::fromString($request->get('query') ?? '');
+        $page = intval($request->get('page', '0')) ?: 1;
+        $count = min(intval($request->get('count', '0')) ?: 20, 100);
+
+        $results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
+
+        /** @var Entity $result */
+        foreach ($results['results'] as $result) {
+            $result->setVisible([
+                'id', 'name', 'slug', 'book_id',
+                'chapter_id', 'draft', 'template',
+                'created_at', 'updated_at',
+                'tags', 'type',
+            ]);
+            $result->setAttribute('type', $result->getType());
+        }
+
+        return response()->json([
+            'data' => $results['results'],
+            'total' => $results['total'],
+        ]);
+    }
+
+
+}
diff --git a/dev/api/requests/search-all.http b/dev/api/requests/search-all.http
new file mode 100644
index 000000000..ee5223816
--- /dev/null
+++ b/dev/api/requests/search-all.http
@@ -0,0 +1 @@
+GET /api/search?query=cats+{created_by:me}&page=1&count=2
\ No newline at end of file
diff --git a/dev/api/responses/search-all.json b/dev/api/responses/search-all.json
new file mode 100644
index 000000000..3096d6da1
--- /dev/null
+++ b/dev/api/responses/search-all.json
@@ -0,0 +1,52 @@
+{
+  "data": [
+    {
+      "id": 84,
+      "book_id": 1,
+      "slug": "a-chapter-for-cats",
+      "name": "A chapter for cats",
+      "created_at": "2021-11-14T15:57:35.000000Z",
+      "updated_at": "2021-11-14T15:57:35.000000Z",
+      "type": "chapter",
+      "tags": []
+    },
+    {
+      "name": "The hows and whys of cats",
+      "id": 396,
+      "slug": "the-hows-and-whys-of-cats",
+      "book_id": 1,
+      "chapter_id": 75,
+      "draft": false,
+      "template": false,
+      "created_at": "2021-05-15T16:28:10.000000Z",
+      "updated_at": "2021-11-14T15:56:49.000000Z",
+      "type": "page",
+      "tags": [
+        {
+          "name": "Animal",
+          "value": "Cat",
+          "order": 0
+        },
+        {
+          "name": "Category",
+          "value": "Top Content",
+          "order": 0
+        }
+      ]
+    },
+    {
+      "name": "How advanced are cats?",
+      "id": 362,
+      "slug": "how-advanced-are-cats",
+      "book_id": 13,
+      "chapter_id": 73,
+      "draft": false,
+      "template": false,
+      "created_at": "2020-11-29T21:55:07.000000Z",
+      "updated_at": "2021-11-14T16:02:39.000000Z",
+      "type": "page",
+      "tags": []
+    }
+  ],
+  "total": 3
+}
\ No newline at end of file
diff --git a/resources/views/api-docs/parts/endpoint.blade.php b/resources/views/api-docs/parts/endpoint.blade.php
index c1bce805b..6e3d93659 100644
--- a/resources/views/api-docs/parts/endpoint.blade.php
+++ b/resources/views/api-docs/parts/endpoint.blade.php
@@ -13,7 +13,7 @@
 
 @if($endpoint['body_params'] ?? false)
     <details class="mb-m">
-        <summary class="text-muted">Body Parameters</summary>
+        <summary class="text-muted">{{ $endpoint['method'] === 'GET' ? 'Query' : 'Body'  }} Parameters</summary>
         <table class="table">
             <tr>
                 <th>Param Name</th>
diff --git a/routes/api.php b/routes/api.php
index 4ba499462..7876ba6d4 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -9,6 +9,7 @@ use BookStack\Http\Controllers\Api\ChapterApiController;
 use BookStack\Http\Controllers\Api\ChapterExportApiController;
 use BookStack\Http\Controllers\Api\PageApiController;
 use BookStack\Http\Controllers\Api\PageExportApiController;
+use BookStack\Http\Controllers\Api\SearchApiController;
 use Illuminate\Support\Facades\Route;
 
 /**
@@ -57,6 +58,8 @@ Route::get('pages/{id}/export/pdf', [PageExportApiController::class, 'exportPdf'
 Route::get('pages/{id}/export/plaintext', [PageExportApiController::class, 'exportPlainText']);
 Route::get('pages/{id}/export/markdown', [PageExportApiController::class, 'exportMarkDown']);
 
+Route::get('search', [SearchApiController::class, 'all']);
+
 Route::get('shelves', [BookshelfApiController::class, 'list']);
 Route::post('shelves', [BookshelfApiController::class, 'create']);
 Route::get('shelves/{id}', [BookshelfApiController::class, 'read']);
diff --git a/tests/Api/SearchApiTest.php b/tests/Api/SearchApiTest.php
new file mode 100644
index 000000000..55ca0e009
--- /dev/null
+++ b/tests/Api/SearchApiTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Tests\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class SearchApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected $baseEndpoint = '/api/search';
+
+    public function test_all_endpoint_returns_search_filtered_results_with_query()
+    {
+        $this->actingAsApiEditor();
+        $uniqueTerm = 'MySuperUniqueTermForSearching';
+
+        /** @var Entity $entityClass */
+        foreach ([Page::class, Chapter::class, Book::class, Bookshelf::class] as $entityClass) {
+            /** @var Entity $first */
+            $first = $entityClass::query()->first();
+            $first->update(['name' => $uniqueTerm]);
+            $first->indexForSearch();
+        }
+
+        $resp = $this->getJson($this->baseEndpoint . '?query=' . $uniqueTerm . '&count=5&page=1');
+        $resp->assertJsonCount(4, 'data');
+        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'book']);
+        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'chapter']);
+        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'page']);
+        $resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'bookshelf']);
+    }
+
+    public function test_all_endpoint_requires_query_parameter()
+    {
+        $resp = $this->actingAsApiEditor()->get($this->baseEndpoint);
+        $resp->assertStatus(422);
+
+        $resp = $this->actingAsApiEditor()->get($this->baseEndpoint . '?query=myqueryvalue');
+        $resp->assertOk();
+    }
+}