Quickstart
Goal: list the published entries of one content type on your site from your terminal.
What you’ll have when you’re done
Section titled “What you’ll have when you’re done”- The machine name of a content type, read from your site’s JSON:API 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
Section titled “Prerequisites”- The API client from the auth quickstart: you’ll reuse its
.envfile. - Node.js 20.6 or later for the JSON:API Client and JavaScript paths, or
curl(preinstalled on macOS and Linux).
-
Find your content type’s machine name
Section titled “Find your content type’s machine name”In your site’s left sidebar, go to
API > JSON:API Query Builder. In theEntity Typedropdown select the content entity type (node), then pick your content type in theBundledropdown.The
Request URLsection now shows the exact URL for that type. The last path segment is the machine name:https://your-site.example.com/api/node/articleThe machine name (
articlehere) is what the API uses. It is lowercase with underscores, and often differs from the label editors see: a type labeled “News post” might benews_post. The steps below usearticle; substitute yours.No browser handy? The API root you called at the end of the auth quickstart already lists every type: look for the
linksentries startingnode--: the part after the double dash is the machine name. -
List the entries
Section titled “List the entries”Call the collection endpoint with the credentials from the auth quickstart’s
.env:The 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’stypemember uses). In a directory with the.envfile:Terminal window npm install @drupal-api-client/json-api-clientlist-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 /apiapiPrefix: "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));Terminal window node --env-file=.env list-articles.mjsNo token handling: the client exchanges the credentials for a token itself and refreshes it when it expires.
Get a token, then
fetchthe collection URL with it. In a directory with the.envfile: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),);Terminal window node --env-file=.env list-articles.mjsLoad your
.envinto the shell, get a token, and call the collection endpoint:Terminal window set -a; source .env; set +a # export every variable in .env into this shellTOKEN=$(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 failedcurl -s "$DRUPAL_SITE_URL/api/node/article" \-H "Accept: application/vnd.api+json" \-H "Authorization: Bearer $TOKEN"If
echo $TOKENprints nothing, the token request failed and the next call would 401 two steps from the real error: re-run the tokencurlwithout the trailing| sed …to see the error response itself.A
200response returns the first page of published entries of that type, up to thepage[limit]default of 50. Beyond that, followlinks.next. (The API sends one compact line; shown formatted and trimmed here.){"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 values, keyed by field machine name.title,created,changed,status,langcode, andpathappear 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 hasnode_typeanduid; reference fields your type defines (media, taxonomy terms) appear here too. The querying guide shows how to pull them into the same response.
-
Limit the result with
Section titled “Limit the result with page[limit]”page[limit]Add one query parameter:
Query parameters are built with the
drupal-jsonapi-paramspackage, the client’s companion for everything you’d otherwise hand-write into the URL:Terminal window npm install drupal-jsonapi-params// add to list-articles.mjsimport {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()producespage[limit]=2(URL-encoded), so you never escape brackets yourself.Append the parameter to the URL;
fetchencodes the square brackets for you: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),);Add the parameter, and add
-gto curl, which stops it treating the square brackets as glob characters:Terminal window curl -g -s "$DRUPAL_SITE_URL/api/node/article\?page[limit]=2" \-H "Accept: application/vnd.api+json" \-H "Authorization: Bearer $TOKEN"datanow holds at most 2 entries, and when more exist,linksgains ready-madenextandlastURLs:{"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.
What just happened
Section titled “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
Section titled “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/pagewhile your content isnode/article). Check: re-select the entity type and bundle in the Query Builder and compare itsRequest URLwith 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
datais empty (or shorter than the response’smeta.count) and the response carries ameta.omittedblock ("detail": "Some resources have been omitted because of insufficient authorization."), the entries are unpublished or access-restricted, not missing. Checkmeta.omittedbefore concluding there’s no content. Entries created via the API are unpublished by default. See the writing content guide.
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.
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
Section titled “Next steps”- First fetch: turn this call into your first rendered page.
- Querying guide: filters, sparse fieldsets, related entries, pagination, and sorting, composed into real queries.
API > JSON:API Query Builderon your site: build and preview any query visually, then copy the URL or generated code.API > OpenAPI documentationon your site: the interactive list of every endpoint your site exposes.
Was this page helpful?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)