Skip to main content

A search result or chat citation gives you a document URL. To see the title, permalink, and extracted text for that one document, you call Glean's experimental document endpoints, then confirm the document ID in the response reads back the same document. The published TypeScript API client does not include these endpoints, so this quickstart calls the HTTP API and uses the official auth package for sign-in.

You sign in with @gleanwork/auth and call the experimental document endpoints over HTTP. The published TypeScript API client does not include these endpoints. client.documents is a different API.

Call the experimental Documents API

Find your Glean backend, take a documents:read token from the auth package, and send X-Glean-Include-Experimental: true on every request. GET /api/documents reads by URL. GET /api/documents/{document_id} reads by the document ID from the first response, with the path segment encoded.

src/client.ts
import { loadEnvFile } from 'node:process';
import { createGleanTokenProvider, discoverGleanTenant } from '@gleanwork/auth';
import {
readDocumentGetResponse,
type DocumentGetResponse,
} from './documents.js';
import {
DocumentsTimeoutError,
documentsApiError,
readProblemDetail,
} from './errors.js';

// These experimental document endpoints are not in @gleanwork/api-client.
// client.documents is a different API. Call GET /api/documents with fetch.
// Sign in with @gleanwork/auth. Every request sends
// X-Glean-Include-Experimental: true. A 404 with that header usually means
// your Glean instance has not turned the endpoints on. Ask your Glean admin
// or Glean support.
export const DOCUMENTS_SCOPE = 'documents:read';
export const EXPERIMENTAL_HEADER = 'X-Glean-Include-Experimental';

const REQUEST_TIMEOUT_MS = 30_000;
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);

export interface GleanClientTarget {
email?: string;
serverUrl?: string;
}

export interface DocumentsClient {
origin: string;
getDocumentByUrl(
url: string,
includeContent: boolean,
): Promise<DocumentGetResponse>;
getDocumentById(
documentId: string,
includeContent: boolean,
): Promise<DocumentGetResponse>;
}

function loadDotEnv() {
try {
loadEnvFile();
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
}

export function assertBackendOrigin(serverURL: string): string {
let server: URL;
try {
server = new URL(serverURL);
} catch {
throw new Error('Use a complete Glean backend HTTPS origin.');
}
const loopback = LOOPBACK_HOSTS.has(server.hostname);
if (
(server.protocol !== 'https:' && !loopback) ||
server.username ||
server.password ||
server.search ||
server.hash ||
(server.pathname && server.pathname !== '/') ||
(!loopback && server.port)
) {
throw new Error('Use a complete Glean backend HTTPS origin.');
}
return server.origin;
}

async function resolveServerUrl({ email, serverUrl }: GleanClientTarget) {
const explicit = serverUrl?.trim();
if (explicit) return explicit;

const workEmail = email?.trim();
if (workEmail) return (await discoverGleanTenant(workEmail)).serverUrl;

const configured = process.env.GLEAN_SERVER_URL?.trim();
if (configured) return configured;

throw new Error(
'Pass --email or --server-url, or set GLEAN_SERVER_URL in your environment.',
);
}

function parseJson(text: string): unknown {
if (!text) return undefined;
try {
return JSON.parse(text) as unknown;
} catch {
return undefined;
}
}

function isTimeout(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
error.name === 'TimeoutError'
);
}

export async function createDocumentsClient(
target: GleanClientTarget,
log: (message: string) => void = () => undefined,
): Promise<DocumentsClient> {
loadDotEnv();
const origin = assertBackendOrigin(await resolveServerUrl(target));
const staticToken = process.env.GLEAN_API_TOKEN?.trim();
if (staticToken) log('Using GLEAN_API_TOKEN from the environment.');
else log('Using the OAuth session (documents:read).');

const getAccessToken = staticToken
? () => Promise.resolve(staticToken)
: createGleanTokenProvider({
serverUrl: origin,
scopes: [DOCUMENTS_SCOPE],
});

async function request(
url: URL,
init: {
method?: string;
body?: string;
headers?: Record<string, string>;
} = {},
): Promise<unknown> {
const accessToken = await getAccessToken();
let response: Response;
try {
response = await fetch(url, {
method: init.method,
body: init.body,
redirect: 'error',
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
headers: {
accept: 'application/json',
...init.headers,
authorization: `Bearer ${accessToken}`,
[EXPERIMENTAL_HEADER]: 'true',
},
});
} catch (error) {
if (isTimeout(error)) throw new DocumentsTimeoutError();
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Could not reach Glean: ${message}`);
}

const body = parseJson(await response.text());
if (!response.ok) throw documentsApiError(response, body);
if (readProblemDetail(body)) {
throw new Error('Document response was an error document with HTTP 200.');
}
return body;
}

function documentsUrl(path: string, query?: URLSearchParams): URL {
const url = new URL(path, origin);
if (query && [...query.keys()].length > 0) url.search = query.toString();
return url;
}

return {
origin,
async getDocumentByUrl(url, includeContent) {
if (url.trim() === '') throw new Error('url must not be blank.');
const query = new URLSearchParams({ url });
if (includeContent) query.append('include', 'CONTENT');
return readDocumentGetResponse(
await request(documentsUrl('/api/documents', query)),
includeContent,
);
},
async getDocumentById(documentId, includeContent) {
if (documentId.trim() === '') {
throw new Error('document_id must not be blank.');
}
const query = new URLSearchParams();
if (includeContent) query.append('include', 'CONTENT');
return readDocumentGetResponse(
await request(
documentsUrl(
`/api/documents/${encodeURIComponent(documentId)}`,
query,
),
),
includeContent,
);
},
};
}

Round-trip the document ID

Read the URL response, then request that document_id again. Require the same document_id. HTTP 404, which usually means your Glean instance has not turned these endpoints on, fails the command. The permalink in the response may differ from the URL you passed.

src/workflow.ts
import type { DocumentsClient } from './client.js';
import type { DocumentGetResponse } from './documents.js';
import { DocumentsApiError } from './errors.js';

export async function retrieveDocumentByUrl(
client: DocumentsClient,
input: { url: string; includeContent: boolean },
): Promise<{ byUrl: DocumentGetResponse; byId: DocumentGetResponse }> {
const byUrl = await client.getDocumentByUrl(input.url, input.includeContent);
const documentId = byUrl.document.document_id;
let byId: DocumentGetResponse;
try {
byId = await client.getDocumentById(documentId, input.includeContent);
} catch (error) {
if (error instanceof DocumentsApiError) {
throw new Error(
`Retrieved document_id ${documentId} by URL (request ${byUrl.request_id}), but get by ID failed.`,
{ cause: error },
);
}
throw error;
}
if (byId.document.document_id !== documentId) {
throw new Error(
`Get by ID returned document_id ${byId.document.document_id}, which does not match ${documentId} from the URL lookup (request ${byUrl.request_id}).`,
);
}
return { byUrl, byId };
}
Your CLIdocument URL
Auth packagedocuments:read token
Documents APIexperimental get by URL, then by ID
Node.js 22.12.0 or newer
A Glean instance where you can open indexed documents
Your work email, or the complete Glean backend HTTPS origin
The documents:read OAuth scope, or a user-scoped API token created with Read documents (documents:read). On the legacy token screen, choose DOCUMENTS
1

Copy the project onto your machine

Copy the runnable TypeScript CLI and its offline tests into a new directory. Sign-in uses the official @gleanwork/auth package.

npx -y tiged@2.12.8 gleanwork/glean-cookbook/recipes/retrieve-document-by-url retrieve-document-by-url
2

Install dependencies

Enter the project directory and install its dependencies. Run the remaining commands from this directory.

cd retrieve-document-by-url && npm install
3

Run the offline tests

Run the tests. They do not use your Glean login or the network. Passing tests does not prove you can reach your Glean instance.

npm test
4

Choose an authentication path

OAuth is the default. @gleanwork/auth discovers your instance, registers the OAuth client dynamically, and stores credentials outside the project. If OAuth is unavailable, copy .env.example to .env, set GLEAN_SERVER_URL to your backend origin, for example https://<instance>-be.glean.com, and create a user-scoped API token with Read documents (documents:read). On the legacy token screen, choose DOCUMENTS. Put that token in GLEAN_API_TOKEN and skip the next step. Omit --email from the remaining commands. If email discovery finds the wrong instance, replace --email with --server-url and that same backend origin.

5

Sign in with OAuth

Run the login command with the documents:read scope and finish authorization in your browser. Wait until the command reports success. Skip this step when you configured a token in .env.

npm run login -- --email "<work-email>"
6

Verify against your instance

Retrieve one document you can open, then retrieve it again by the returned document_id. The command prints the permalink, title, datasource, and both request IDs. With a token in .env, omit --email.

npm start -- --email "<work-email>" --url "<document-url>"
7

Request extracted text

Run the same retrieval with --content. The command prints content_segments: and a short content_preview: of the first non-empty segment, or the line content: no extracted text. You still see the document when there is no extracted text. If Glean cannot read the text you asked for, the command fails. With a token, omit --email.

npm start -- --email "<work-email>" --url "<document-url>" --content

This client sets X-Glean-Include-Experimental: true on every request. Keep that header if you add a call. A 404 with the header usually means your Glean instance has not turned these endpoints on. Ask your Glean admin or Glean support.

OAuth login and the token provider use the documents:read scope. Do not add other scopes. If you create an API token instead, choose Read documents (documents:read). On the legacy token screen, choose DOCUMENTS.

HTTP 404 exits non-zero and does not print Round-trip document_id matches. It usually means your Glean instance has not turned these endpoints on. Ask your Glean admin or Glean support, and share the Request ID. If the endpoints are on, a 404 usually means the document does not exist or you cannot access it. The batch command lists a missing document ID or URL under warnings and still exits 0.

These endpoints are experimental and can change. This client sends X-Glean-Include-Experimental: true on every request. If your Glean instance has not turned the endpoints on, you still get HTTP 404. Ask your Glean admin or Glean support. The published @gleanwork/api-client package has no methods for these endpoints. client.documents reads documents through a different API. Do not call it here.

A Gmail or Outlook search result or chat citation can show an ID that only works in that result. Pass the message URL instead of that ID. The URL can still return 404. Go links and federated search results cannot be retrieved. You only get documents you can open.

include=CONTENT adds content.full_text_list. An empty list means Glean stored no extracted text. You still see the document. If Glean cannot read the text you asked for, the command fails instead of printing the document without content. The command prints a short preview, not every segment.

Take it further
  • Pass --content when you need extracted text. include=CONTENT adds content.full_text_list. An empty list means Glean stored no extracted text. You still see the document. If Glean cannot read the text you asked for, the command fails.
  • Use the batch recipe when you have many document IDs and URLs. Copy the document_id line this command prints and pass it to --id there. The batch prints documents Glean returns under results, and document IDs or URLs it does not return under warnings.
  • Keep the document_id this command prints. The get-by-id call accepts that ID. For a Gmail or Outlook message, pass the message URL, not the ID from the search result or chat citation.

Retrieve a document URL you can open in Glean

Prints the title, a document_id line, the permalink, datasource, document type, timestamps, and url_request_id, then a line that says Round-trip document_id matches. The permalink may differ from the URL you passed. HTTP errors, including 404, exit non-zero and do not print that success line. A 404 usually means your Glean instance has not turned these endpoints on. If they are on, a 404 usually means the document does not exist or you cannot access it. Ask your Glean admin or Glean support. With --content, the command prints content_segments and content_preview, or the line content: no extracted text.

View source

Runs the recipe through the Glean cookbook plugin.

Auth

Run the authenticate step on this page. It discovers your tenant from work email and signs you in with OAuth, using the shipped login command. If OAuth is unavailable, create a scoped Glean-issued token in Token Management (documents:read).

At a glance
CapabilitiesDocuments
SurfacesPlatform API
StatusQuickstart
Time~15 min
Required scopes
documents:read