# Quickstart

**Goal:** list the published entries of one [content type](/start-here/glossary/#content-type-bundle) on your [site](/start-here/glossary/#site) from your terminal.

## What you'll have when you're done

- The [machine name](/start-here/glossary/#machine-name) of a content type, read from your site's [JSON:API Query Builder](/start-here/glossary/#jsonapi-query-builder).
- One successful `GET /api/node/{type}` request and a map of where entries, IDs, and fields live in the response.
- Proof that query parameters work, by limiting the result with `page[limit]`.

## Prerequisites

- The [API client](/start-here/glossary/#api-client) from the [auth quickstart](/source-cms/authenticate/quickstart/): you'll reuse its `.env` file.
- [Node.js](https://nodejs.org/) 20.6 or later for the JSON:API Client and JavaScript paths, or [`curl`](https://curl.se/) (preinstalled on macOS and Linux).

## Steps

<Steps>

1. ### Find your content type's machine name

   In your site's left sidebar, go to `API > JSON:API Query Builder`. In the `Entity Type` dropdown select the content [entity type](/start-here/glossary/#entity-type) (`node`), then pick your content type in the `Bundle` dropdown.

   The `Request URL` section now shows the exact URL for that type. The last path segment is the machine name:

   ```
   https://your-site.example.com/api/node/article
   ```

   The machine name (`article` here) is what the API uses. It is lowercase with underscores, and often differs from the label editors see: a type labeled "News post" might be `news_post`. The steps below use `article`; substitute yours.

   No browser handy? The API root you called at the end of the [auth quickstart](/source-cms/authenticate/quickstart/) already lists every type: look for the `links` entries starting `node--`: the part after the double dash is the machine name.

2. ### List the entries

   Call the collection endpoint with the credentials from the auth quickstart's `.env`:

   <Tabs syncKey="api-style">
     <TabItem label="JSON:API Client">
       The [Drupal API Client](/start-here/glossary/#drupal-api-client) addresses the type by its resource type string: entity type and machine name joined with a double dash, `node--article` (the same format the response's `type` member uses). In a directory with the `.env` file:

       ```bash
       npm install @drupal-api-client/json-api-client
       ```

       ```js
       // list-articles.mjs
       import {
         JsonApiClient,
       } from "@drupal-api-client/json-api-client";

       const client = new JsonApiClient(
         process.env.DRUPAL_SITE_URL,
         {
           // Source CMS serves JSON:API at /api
           apiPrefix: "api",
           authentication: {
             type: "OAuth",
             credentials: {
               grantType: "client_credentials",
               clientId: process.env.DRUPAL_CLIENT_ID,
               clientSecret: process.env.DRUPAL_CLIENT_SECRET,
             },
           },
         },
       );

       const articles = await client.getCollection(
         "node--article",
       );
       console.log(JSON.stringify(articles, null, 2));
       ```

       ```bash
       node --env-file=.env list-articles.mjs
       ```

       No token handling: the client exchanges the credentials for a token itself and refreshes it when it expires.
     </TabItem>
     <TabItem label="JavaScript">
       Get a token, then `fetch` the collection URL with it. In a directory with the `.env` file:

       ```js
       // list-articles.mjs
       const {
         DRUPAL_SITE_URL,
         DRUPAL_CLIENT_ID,
         DRUPAL_CLIENT_SECRET,
       } = process.env;

       const tokenHeaders = {
         "Content-Type": "application/x-www-form-urlencoded",
       };

       const tokenResponse = await fetch(
         `${DRUPAL_SITE_URL}/oauth/token`,
         {
           method: "POST",
           headers: tokenHeaders,
           body: new URLSearchParams({
             grant_type: "client_credentials",
             client_id: DRUPAL_CLIENT_ID,
             client_secret: DRUPAL_CLIENT_SECRET,
           }),
         }
       );
       const { access_token } = await tokenResponse.json();

       const response = await fetch(
         `${DRUPAL_SITE_URL}/api/node/article`,
         {
           headers: {
             Accept: "application/vnd.api+json",
             Authorization: `Bearer ${access_token}`,
           },
         }
       );
       console.log(
         JSON.stringify(await response.json(), null, 2),
       );
       ```

       ```bash
       node --env-file=.env list-articles.mjs
       ```
     </TabItem>
     <TabItem label="curl">
       Load your `.env` into the shell, get a token, and call the collection endpoint:

       ```bash
       set -a; source .env; set +a  # export every variable in .env into this shell

       TOKEN=$(curl -s -X POST \
         "$DRUPAL_SITE_URL/oauth/token" \
         -H "Content-Type: application/x-www-form-urlencoded" \
         -d "grant_type=client_credentials" \
         -d "client_id=$DRUPAL_CLIENT_ID" \
         -d "client_secret=$DRUPAL_CLIENT_SECRET" \
         | sed -n 's/.*"access_token": *"\([^"]*\)".*/\1/p')

       echo $TOKEN  # a long string; empty means the token request failed

       curl -s "$DRUPAL_SITE_URL/api/node/article" \
         -H "Accept: application/vnd.api+json" \
         -H "Authorization: Bearer $TOKEN"
       ```

       If `echo $TOKEN` prints nothing, the token request failed and the next call would 401 two steps from the real error: re-run the token `curl` without the trailing `| sed …` to see the error response itself.
     </TabItem>
   </Tabs>

   A `200` response returns the first page of published entries of that type, up to the [`page[limit]` default of 50](/source-cms/reference/query-parameters/#pagelimit-and-pageoffset-pagination). Beyond that, follow `links.next`. (The API sends one compact line; shown formatted and trimmed here.)

   ```json
   {
     "jsonapi": {
       "version": "1.1",
       "meta": {
         "links": {
           "self": {
             "href": "http://jsonapi.org/format/1.1/"
           }
         }
       }
     },
     "data": [
       {
         "type": "node--article",
         "id": "3f9a2b1e-6c4d-4a8e-9b7f-1d2e3c4b5a69",
         "links": {
           "self": {
             "href": "https://your-site.example.com/api/node/article/3f9a2b1e-6c4d-4a8e-9b7f-1d2e3c4b5a69?resourceVersion=id%3A1"
           }
         },
         "attributes": {
           "langcode": "en",
           "status": true,
           "title": "Spring launch recap",
           "created": "2026-05-11T09:30:52+00:00",
           "changed": "2026-05-12T14:02:10+00:00",
           "path": {
             "alias": null,
             "pid": null,
             "langcode": "en"
           }
         },
         "relationships": {
           "node_type": { "…": "…" },
           "uid": { "…": "…" }
         }
       },
       {
         "type": "node--article",
         "id": "…more entries…",
         "attributes": { "…": "…" }
       }
     ],
     "meta": { "count": 5 },
     "links": { "self": {
      "href": "https://your-site.example.com/api/node/article"
     } }
   }
   ```

   Where to look:

   - **`data`**: the entries. One object per entry.
   - **`type`**: entity type plus bundle machine name, joined with a double dash: `node--article`.
   - **`id`**: the entry's UUID, its stable identifier. One entry lives at `/api/node/article/{id}`.
   - **`attributes`**: the entry's [field](/start-here/glossary/#field) values, keyed by field machine name. `title`, `created`, `changed`, `status`, `langcode`, and `path` appear on every content type; your type's own fields appear alongside them, so your list will be longer and different.
   - **`relationships`**: pointers to related entries and metadata. Every type has `node_type` and `uid`; reference fields your type defines (media, taxonomy terms) appear here too. The [querying guide](/source-cms/content-api/guide/) shows how to pull them into the same response.

3. ### Limit the result with `page[limit]`

   Add one query parameter:

   <Tabs syncKey="api-style">
     <TabItem label="JSON:API Client">
       Query parameters are built with the `drupal-jsonapi-params` package, the client's companion for everything you'd otherwise hand-write into the URL:

       ```bash
       npm install drupal-jsonapi-params
       ```

       ```js
       // add to list-articles.mjs
       import {
         DrupalJsonApiParams,
       } from "drupal-jsonapi-params";

       const queryString = new DrupalJsonApiParams()
         .addPageLimit(2)
         .getQueryString();
       const limited = await client.getCollection(
         "node--article",
         { queryString },
       );
       console.log(JSON.stringify(limited, null, 2));
       ```

       `getQueryString()` produces `page[limit]=2` (URL-encoded), so you never escape brackets yourself.
     </TabItem>
     <TabItem label="JavaScript">
       Append the parameter to the URL; `fetch` encodes the square brackets for you:

       ```js
       const limited = await fetch(
         `${DRUPAL_SITE_URL}/api/node/article?page[limit]=2`,
         {
           headers: {
             Accept: "application/vnd.api+json",
             Authorization: `Bearer ${access_token}`,
           },
         }
       );
       console.log(
         JSON.stringify(await limited.json(), null, 2),
       );
       ```
     </TabItem>
     <TabItem label="curl">
       Add the parameter, and add `-g` to curl, which stops it treating the square brackets as glob characters:

       ```bash
       curl -g -s "$DRUPAL_SITE_URL/api/node/article\
       ?page[limit]=2" \
         -H "Accept: application/vnd.api+json" \
         -H "Authorization: Bearer $TOKEN"
       ```
     </TabItem>
   </Tabs>

   `data` now holds at most 2 entries, and when more exist, `links` gains ready-made `next` and `last` URLs:

   ```json
   {
     "data": [
       { "type": "node--article", "…": "…" },
       { "type": "node--article", "…": "…" }
     ],
     "links": {
       "last": {
         "href": "https://your-site.example.com/api/node/article?page%5Boffset%5D=4&page%5Blimit%5D=2"
       },
       "next": {
         "href": "https://your-site.example.com/api/node/article?page%5Boffset%5D=2&page%5Blimit%5D=2"
       },
       "self": {
         "href": "https://your-site.example.com/api/node/article?page%5Blimit%5D=2"
       }
     }
   }
   ```

   That's the whole mechanism: everything you will ever ask the Content API for (filters, fields, related entries, sorting) is a query parameter on a `GET`, shaping the same response you just read.

</Steps>

## What just happened

`GET /api/node/{type}` is the collection endpoint for one content type, and query parameters shape what it returns. Your site ships two companion tools for going further. The `JSON:API Query Builder` (`API > JSON:API Query Builder`) builds these queries visually (fields, filters, includes, sort), previews live responses, and generates code samples. The per-site `OpenAPI documentation` (`API > OpenAPI documentation`) lists every endpoint your site exposes, with interactive try-it-out calls.

## When something goes wrong

**`"data": []` with `200 OK`**: the request worked but returned nothing. Three distinct causes:

- *You queried the wrong type.* The URL names a real type, but not the one your entries belong to (for example `node/page` while your content is `node/article`). Check: re-select the entity type and bundle in the Query Builder and compare its `Request URL` with yours; remember it's the machine name, not the label, and it's case-sensitive lowercase.
- *The type has no published content.* The name is right but nothing is published. Check: open your site's content list, filter to that type, and confirm at least one entry is published.
- *The entries exist but you're not allowed to see them.* When `data` is empty (or shorter than the response's `meta.count`) and the response carries a `meta.omitted` block (`"detail": "Some resources have been omitted because of insufficient authorization."`), the entries are unpublished or access-restricted, not missing. Check `meta.omitted` before concluding there's no content. Entries created via the API are unpublished by default. See the [writing content guide](/source-cms/content-api/writing-content/).

**`404 Not Found`**: the type in the URL doesn't exist at all: a label used in place of the machine name (`node/Article`, `node/news post`), wrong case, or a typo. Copy the URL from the Query Builder's `Request URL` section instead of typing it.

**`401 Unauthorized`**: the token is missing or expired (tokens expire after `expires_in` seconds: 300, so 5 minutes). In the JavaScript path, rerun the script for a fresh token; in the curl path, re-run the `TOKEN=` command from step 2 in the same shell. The JSON:API Client refreshes tokens itself, so a 401 there points at the credentials. More cases in the [auth guide's troubleshooting](/source-cms/authenticate/guide/#when-something-goes-wrong).

**`SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON`** from the JSON:API Client: the client called its default base path `/jsonapi` and got an HTML page back. Source CMS serves the API at `/api`; construct the client with `apiPrefix: "api"`, as in step 2.

**`curl: (3) bad range in URL position …`**: curl interpreted the `[` `]` in `page[limit]` as glob syntax. Add `-g` (globoff), as in step 3. Note that `-s` silences this error entirely: if a bracketed request prints nothing at all, rerun it without `-s` to see what curl is complaining about.

## Next steps

- [First fetch](/source-cms/content-api/first-fetch/): turn this call into your first rendered page.
- [Querying guide](/source-cms/content-api/guide/): filters, sparse fieldsets, related entries, pagination, and sorting, composed into real queries.
- `API > JSON:API Query Builder` on your site: build and preview any query visually, then copy the URL or generated code.
- `API > OpenAPI documentation` on your site: the interactive list of every endpoint your site exposes.
