Skip to content

Fix: Scalar Not Working — API Docs Not Rendering, Try-It Not Sending Requests, or Theme Broken

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix Scalar API documentation issues — OpenAPI spec loading, interactive Try-It panel, authentication configuration, custom themes, CDN and React integration, and self-hosting.

The Problem

Scalar renders a blank page or shows a loading spinner forever:

<script id="api-reference" data-url="./openapi.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<!-- Blank page — no documentation visible -->

Or the “Try It” panel sends requests but they fail:

CORS error: Access to fetch blocked by CORS policy

Or the authentication section doesn’t pre-fill tokens:

Every request returns 401 even after entering the API key

Why This Happens

Scalar is an API documentation tool that renders interactive docs from OpenAPI specifications. It displays endpoints, schemas, and provides a “Try It” panel for live API testing.

The thing that makes Scalar (and ReDoc, and Swagger UI) feel unreliable is that the docs portal is a thin client over a contract you generate elsewhere. A “broken docs” report is almost never a Scalar bug. It is one of: the spec file is unreachable, the spec is reachable but invalid, the spec is valid but stale, or the spec is valid and fresh but Try-It cannot reach your API because of CORS. Each of those lives in a different system owned by a different team, which is why partner-integration tickets that reach “your docs are down” so often loop for days before anyone identifies the actual layer.

  • The OpenAPI spec must be accessible — Scalar fetches the spec file via HTTP. If the URL is wrong, the file returns 404, or CORS blocks the request, Scalar can’t load the spec and shows a blank page.
  • Try-It requests go directly from the browser — the browser sends API requests to your server. If the API doesn’t include CORS headers (Access-Control-Allow-Origin), the browser blocks the response. This isn’t a Scalar issue — it’s a server configuration issue.
  • Authentication values must be configured — Scalar reads securitySchemes from the OpenAPI spec to show auth fields, but it doesn’t automatically fill in values. You must configure authentication through the Scalar options or have the user enter credentials manually.
  • The spec must be valid OpenAPI 3.x — Scalar supports OpenAPI 3.0 and 3.1. Swagger 2.0 specs need conversion. Invalid specs cause partial renders or missing endpoints.

A subtler class of failure is spec drift. The spec is generated at build time from Zod schemas or ts-rest contracts, but the running API has moved on — a new endpoint was added in a hotfix, a query parameter was renamed, an error response shape changed. The docs render successfully but describe a version of the API that no longer exists. Consumers writing client code against the published docs hit confused 400 responses and lose trust in the portal. Treat the spec as a build artifact that ships in lockstep with the API, not as a document maintained by hand.

Fix 1: Basic Setup (CDN)

<!-- Simplest setup — single HTML file -->
<!DOCTYPE html>
<html>
<head>
  <title>API Documentation</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
  <!-- Reference your OpenAPI spec -->
  <script
    id="api-reference"
    data-url="https://api.myapp.com/openapi.json"
    data-proxy-url="https://proxy.scalar.com"
  ></script>

  <!-- Load Scalar -->
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
<!-- Inline spec instead of URL -->
<script id="api-reference" type="application/json">
{
  "openapi": "3.1.0",
  "info": { "title": "My API", "version": "1.0.0" },
  "paths": {
    "/api/users": {
      "get": {
        "summary": "List users",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/User" }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" }
        }
      }
    }
  }
}
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>

Fix 2: React Integration

npm install @scalar/api-reference-react
// app/docs/page.tsx — Next.js App Router
'use client';

import { ApiReferenceReact } from '@scalar/api-reference-react';
import '@scalar/api-reference-react/style.css';

export default function ApiDocs() {
  return (
    <ApiReferenceReact
      configuration={{
        spec: {
          url: '/api/openapi',  // URL to your OpenAPI spec
          // Or inline content:
          // content: openApiSpec,
        },
        // Theme
        theme: 'kepler',  // 'default' | 'alternate' | 'moon' | 'purple' | 'solarized' | 'kepler' | 'saturn' | 'bluePlanet' | 'deepSpace' | 'mars'

        // Hide certain sections
        hideModels: false,
        hideDownloadButton: false,

        // Pre-configured authentication
        authentication: {
          preferredSecurityScheme: 'bearerAuth',
          http: {
            bearer: {
              token: '',  // Pre-fill token
            },
          },
          // Or API key
          apiKey: {
            token: '',
          },
        },

        // Customize base URL for Try It
        servers: [
          { url: 'https://api.myapp.com', description: 'Production' },
          { url: 'http://localhost:3000', description: 'Local' },
        ],
      }}
    />
  );
}

Fix 3: Serve OpenAPI Spec from Next.js

// app/api/openapi/route.ts — dynamic OpenAPI spec
export async function GET() {
  const spec = {
    openapi: '3.1.0',
    info: {
      title: 'My API',
      version: '1.0.0',
      description: 'API documentation for My App',
    },
    servers: [
      { url: 'https://api.myapp.com', description: 'Production' },
      { url: 'http://localhost:3000', description: 'Development' },
    ],
    paths: {
      '/api/users': {
        get: {
          tags: ['Users'],
          summary: 'List all users',
          parameters: [
            {
              name: 'page',
              in: 'query',
              schema: { type: 'integer', default: 1 },
            },
            {
              name: 'limit',
              in: 'query',
              schema: { type: 'integer', default: 20 },
            },
          ],
          responses: {
            '200': {
              description: 'List of users',
              content: {
                'application/json': {
                  schema: {
                    type: 'object',
                    properties: {
                      users: { type: 'array', items: { $ref: '#/components/schemas/User' } },
                      total: { type: 'integer' },
                    },
                  },
                },
              },
            },
          },
        },
        post: {
          tags: ['Users'],
          summary: 'Create a user',
          security: [{ bearerAuth: [] }],
          requestBody: {
            required: true,
            content: {
              'application/json': {
                schema: { $ref: '#/components/schemas/CreateUser' },
              },
            },
          },
          responses: {
            '201': {
              description: 'User created',
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/User' },
                },
              },
            },
          },
        },
      },
    },
    components: {
      schemas: {
        User: {
          type: 'object',
          properties: {
            id: { type: 'string', format: 'uuid' },
            name: { type: 'string' },
            email: { type: 'string', format: 'email' },
            createdAt: { type: 'string', format: 'date-time' },
          },
          required: ['id', 'name', 'email'],
        },
        CreateUser: {
          type: 'object',
          properties: {
            name: { type: 'string', minLength: 1, maxLength: 100 },
            email: { type: 'string', format: 'email' },
          },
          required: ['name', 'email'],
        },
      },
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
        apiKey: {
          type: 'apiKey',
          in: 'header',
          name: 'X-API-Key',
        },
      },
    },
  };

  return Response.json(spec, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Cache-Control': 'public, max-age=3600',
    },
  });
}

Fix 4: Hono Integration

npm install @scalar/hono-api-reference
import { Hono } from 'hono';
import { apiReference } from '@scalar/hono-api-reference';

const app = new Hono();

// Serve OpenAPI spec
app.get('/openapi.json', (c) => {
  return c.json(openApiSpec);
});

// Serve Scalar docs
app.get(
  '/docs',
  apiReference({
    spec: { url: '/openapi.json' },
    theme: 'kepler',
  }),
);

// Your API routes
app.get('/api/users', (c) => { /* ... */ });

Fix 5: Fix CORS for Try-It

The Try-It panel sends requests from the browser. Your API needs CORS headers:

// Next.js middleware — add CORS headers
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const response = NextResponse.next();

    response.headers.set('Access-Control-Allow-Origin', '*');
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');

    if (request.method === 'OPTIONS') {
      return new Response(null, { status: 204, headers: response.headers });
    }

    return response;
  }
}

// Or use Scalar's proxy for production APIs
// data-proxy-url="https://proxy.scalar.com"

Fix 6: Generate OpenAPI from Code

# From Zod schemas
npm install zod-to-openapi

# From ts-rest contracts
npm install @ts-rest/open-api
// Generate from Zod schemas
import { extendZodWithOpenApi, OpenAPIRegistry, OpenApiGeneratorV31 } from 'zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

const registry = new OpenAPIRegistry();

// Register schemas
const UserSchema = registry.register('User', z.object({
  id: z.string().uuid().openapi({ example: '123e4567-e89b-12d3-a456-426614174000' }),
  name: z.string().openapi({ example: 'Alice' }),
  email: z.string().email().openapi({ example: '[email protected]' }),
}));

// Register endpoints
registry.registerPath({
  method: 'get',
  path: '/api/users',
  tags: ['Users'],
  summary: 'List users',
  responses: {
    200: {
      description: 'List of users',
      content: { 'application/json': { schema: z.array(UserSchema) } },
    },
  },
});

// Generate spec
const generator = new OpenApiGeneratorV31(registry.definitions);
const spec = generator.generateDocument({
  openapi: '3.1.0',
  info: { title: 'My API', version: '1.0.0' },
});

Production Incident: The Docs Portal Goes Dark

The incident sounds small but the blast radius is large: the API documentation portal at docs.api.example.com returns a blank page. Your status page reports “all systems operational” because the API itself is healthy, the production database is healthy, and customer traffic to the live API is unaffected. Internally nothing looks wrong.

Externally it is a different story. Partner integrators trying to onboard against your API cannot read endpoint definitions. Existing customers who hit a 4xx and try to recheck the contract see nothing. AI coding assistants that scrape your docs for context start hallucinating endpoints. Support tickets do not say “your docs are down” — they say “your API rejected my request,” because integrators assume the doc is the API. You will spend the first hour of the incident debugging the wrong layer.

A real failure pattern: the spec is generated at build time and uploaded to a CDN bucket; a Terraform change rotated the bucket’s CORS configuration; the spec object is still there but the browser fetch from Scalar gets blocked by a preflight failure. Same root cause, different surface: the build job that uploads the spec ran with a stale credential and silently uploaded a zero-byte file. The portal renders, parses an empty document, and shows nothing.

Three monitoring habits keep this contained. First, synthetic-check the docs portal endpoint and the spec endpoint independently, every minute, from outside your network. If either returns non-200 or content-length below a sane floor, page on it. Second, include a spec-version header or banner that displays the API version and build commit; if integrators see a stale version, they can self-diagnose. Third, schema-validate the spec inside CI before it ships — a broken spec should fail the deploy, not the docs portal.

The fix order during the incident is: confirm the spec URL returns valid JSON in the browser; confirm CORS allows the docs origin; confirm the version in the spec matches the deployed API. If all three pass and the portal still renders blank, then suspect Scalar itself — and even then, the fix is usually pinning the CDN-loaded Scalar bundle to a known-good version to rule out a JS regression.

Still Not Working?

Blank page with no errors — the OpenAPI spec URL is unreachable. Check the browser’s Network tab for 404 or CORS errors on the spec fetch. If the spec is on a different domain, it needs CORS headers. Use the data-proxy-url attribute to route through Scalar’s proxy.

Try-It returns CORS errors — the API server must send Access-Control-Allow-Origin headers. For local development, add CORS middleware. For production APIs that can’t add CORS, use Scalar’s built-in proxy: add data-proxy-url="https://proxy.scalar.com" to the script tag.

Auth token not sent with requests — verify the OpenAPI spec has securitySchemes defined and that endpoints reference them with security: [{ bearerAuth: [] }]. Without the security declaration, Scalar doesn’t show the auth input or include tokens in requests.

Some endpoints are missing from the docs — Scalar renders every path in the OpenAPI spec. Missing endpoints mean they’re not in the spec. If using code generation (Zod, ts-rest), re-run the generator. Also check for tags filtering — if the spec has tags and you’ve configured tag filtering, some endpoints may be hidden.

Spec loads but renders only the navigation, not the bodies — usually $ref resolution failure. If your spec splits across multiple files ($ref: './schemas/User.yaml'), bundle them before serving — Scalar resolves $ref against a single document. Use @apidevtools/swagger-cli bundle or redocly bundle to flatten the spec at build time.

Try-It works locally but not in production — production likely sits behind an auth-only API gateway that rejects unauthenticated preflight requests. Either expose a public read-only endpoint for Try-It demos, or use Scalar’s proxy URL so requests originate from a known CORS-allowed origin.

Spec validates but partner integrations fail anyway — silent spec drift. The spec describes endpoints the running API no longer matches. Add a contract test in CI: hit each documented endpoint with example data from the spec and assert the response shape matches the documented response. Failing contract tests catch drift before integrators do.

For related API documentation issues, see Fix: ts-rest Not Working, Fix: Orval Not Working, Fix: NestJS Swagger Not Showing, and Fix: Hono Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles