In this article
May 20, 2026
May 20, 2026

How to add API key support to your app

Humans, scripts, and AI agents are all calling your API. Here's how to give each of them secure, scoped credentials without building key management from scratch.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

Your users want to build on top of your product. They have scripts to run, workflows to automate, and internal tools to wire together. All of that requires one thing from you: a secure, reliable way to authenticate programmatically against your API.

API keys are the standard answer. They are simple to issue, easy to use in any HTTP client, and familiar to every developer who has ever worked with a third-party API. But building the plumbing behind them is more work than it looks. You need to figure out how to store keys safely, how to scope them correctly in a multi-tenant environment, how to let users create and revoke their own keys without calling your support team, and how to validate those keys on every inbound request without slowing your API down.

WorkOS handles all of that. This guide walks through how to add self-serve API key management to your SaaS, from configuring permissions in the dashboard to validating keys on your backend routes, using AuthKit's built-in API key primitives.

What you are building

We will use a fictional project management SaaS called "Runway" as our working example throughout this guide. Runway lets teams track projects and tasks. Users want to integrate Runway with their own tools: pulling task data into a custom dashboard, pushing updates from a CI pipeline, syncing records into a data warehouse.

By the end of this guide, Runway will:

  • Let users create and revoke their own API keys from a settings page
  • Scope each key to a specific set of permissions (read-only vs full access)
  • Validate keys on protected API routes
  • Give admins visibility over all active keys across the organization
  • Emit auditable events every time a key is created or revoked

Choosing the right credential shape

Before touching any code, it is worth pausing on a question the docs sometimes gloss over: should these keys be scoped to a user or to an organization?

WorkOS supports both, and the answer shapes everything downstream.

User-scoped API keys belong to an individual. The user creates them, the permissions are bounded by what that user is allowed to do, and revoking the user's account revokes their keys automatically. This is the right choice when your users are building personal integrations or automations that act on their own behalf. If a developer at a company wants to pull their own assigned tasks into a personal dashboard, a user-scoped key is the correct primitive.

Org-scoped API keys belong to the organization rather than any individual member. Admins configure which permissions are available for org keys, and each key carries only the subset it actually needs. There is no individual owner, which means offboarding a team member does not disrupt a shared integration, but it also means attribution requires a bit more care on your side. This is the right choice for shared backend integrations: a CI pipeline authenticating as the org, a data sync job running on a schedule, an internal tool shared across the whole team.

For Runway, we will support both. Individual users can create keys for personal scripts. Teams can create org-level keys for shared pipelines.

If you are building something where agents or backend services need to authenticate without any user involvement, WorkOS also offers M2M Applications, which issue short-lived JWTs via the OAuth 2.0 client credentials flow. That is a separate primitive covered in the machine identity guide.

Setting up permissions

API keys in WorkOS are tied to the permissions system, so the first step is deciding what permissions your keys can carry.

Open your WorkOS dashboard and navigate to Authorization > Configuration. You will see a section for API key permissions. This is where you define the permission values that are valid for API keys in your environment.

For Runway, we will configure these permissions:

  • projects:read — read access to projects and tasks
  • projects:write — create and update projects and tasks
  • tasks:read — read access to tasks only
  • tasks:write — create and update tasks

This list controls what any API key can carry at most. Users cannot create a key with a permission you have not added here, which keeps the scope surface well-defined.

Next, make sure the right roles can manage keys. Under Authorization > Roles, the permissions to assign depend on which key scope you are enabling.

  • For org-scoped keys, assign widgets:api-keys:manage to any role that should be able to create and revoke organization API keys.
  • For user-scoped keys, assign widgets:user-api-keys:manage-self to roles that should be able to create and revoke their own keys, and widgets:user-api-keys:manage-all to admin roles that need visibility across every user's keys in the organization. All of these are built-in WorkOS permissions; you do not need to define them yourself.

Adding the API keys widget to your settings page

With permissions configured, the next step is giving users a place to manage their keys. WorkOS provides an <ApiKeys /> widget that handles the full UI: creating keys, selecting permissions, viewing active keys with masked values, and revoking them. It comes from the @workos-inc/widgets package and needs to be wrapped in <WorkOsWidgets>.

Here is how to embed it in a settings page using the access token from AuthKit's useAuth hook:

  
// app/settings/api-keys/page.tsx
'use client';

import { useAuth } from '@workos-inc/authkit-react';
import { ApiKeys, WorkOsWidgets } from '@workos-inc/widgets';

export default function ApiKeysPage() {
  const { isLoading, user, getAccessToken } = useAuth();

  if (isLoading) return null;
  if (!user) return <p>Sign in to manage API keys.</p>;

  return (
    <div className="max-w-2xl mx-auto py-10 px-4">
      <h1 className="text-xl font-semibold mb-6">API keys</h1>
      <p className="text-sm text-gray-500 mb-8">
        Use API keys to authenticate requests to the Runway API from your own
        tools and scripts. Keys are shown once at creation and cannot be
        retrieved afterward.
      </p>
      <WorkOsWidgets>
        <ApiKeys authToken={getAccessToken} scope="user" />
      </WorkOsWidgets>
    </div>
  );
}
  

The authToken prop accepts either a static token string fetched server-side or, as shown above, the getAccessToken function from useAuth directly. The widget calls it when it needs a fresh token.

Pass scope="user" for user-owned keys or scope="organization" for org-level keys. The widget reads the current session context automatically to associate the key with the right user or organization.

When a user creates a key, the widget displays the raw value exactly once. After that, it is masked. This is intentional. WorkOS stores keys as hashes with no reversible path to the original, so there is no way to surface the plaintext again after the initial display. Your users should treat the creation moment as their only chance to copy the key into a secrets manager.

For an org-level key page accessible to admins, the setup is nearly identical, just with scope="organization" and a user who holds the widgets:api-keys:manage permission:

  
// app/settings/org-api-keys/page.tsx
'use client';

import { useAuth } from '@workos-inc/authkit-react';
import { ApiKeys, WorkOsWidgets } from '@workos-inc/widgets';

export default function OrgApiKeysPage() {
  const { isLoading, user, getAccessToken } = useAuth();

  if (isLoading) return null;
  if (!user) return <p>Sign in to manage API keys.</p>;

  return (
    <div className="max-w-2xl mx-auto py-10 px-4">
      <h1 className="text-xl font-semibold mb-6">Organization API keys</h1>
      <p className="text-sm text-gray-500 mb-8">
        Organization keys are shared across your team and are not tied to any
        individual account. Use them for CI pipelines, scheduled jobs, and
        shared integrations.
      </p>
      <WorkOsWidgets>
        <ApiKeys authToken={getAccessToken} />
      </WorkOsWidgets>
    </div>
  );
}
  

Protecting your API routes

With keys in your users' hands, the next step is making your backend accept them. WorkOS provides a validateApiKey helper that verifies a key, returns the associated organization or user, and surfaces the permissions attached to it.

Here is a protected route in Next.js that accepts API key authentication:

  
// app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { validateApiKey } from '@workos-inc/authkit-nextjs';

export async function GET(request: NextRequest) {
  const { apiKey } = await validateApiKey();

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  // Check that the key has the permission this route requires
  if (!apiKey.permissions.includes('projects:read')) {
    return NextResponse.json(
      { error: 'Forbidden: this key does not have the projects:read permission' },
      { status: 403 }
    );
  }

  const projects = await getProjectsForOrg(apiKey.organizationId);
  return NextResponse.json({ projects });
}
  

The validateApiKey call extracts the bearer token from the incoming Authorization header, verifies it against WorkOS, and returns the full API key object if valid. That object includes organizationId, userId (for user-scoped keys), and permissions. You can use the permissions array to enforce fine-grained authorization at the route level.

For an Express backend, the pattern is the same using the Node SDK:

  
// routes/tasks.ts
import express from 'express';
import WorkOS from '@workos-inc/node';

const workos = new WorkOS(process.env.WORKOS_API_KEY);
const router = express.Router();

router.get('/tasks', async (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const token = authHeader.slice(7);

  const { apiKey } = await workos.apiKeys.validateApiKey({
    value: token,
  });

  if (!apiKey) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  if (!apiKey.permissions.includes('tasks:read')) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const tasks = await getTasksForOrg(apiKey.organizationId);
  res.json({ tasks });
});
  

Two things worth noting about the validation response:

  • The permissions array contains only what was explicitly assigned to this key at creation, not the full permission set of the user or org. This is what makes fine-grained key scoping meaningful in practice: a read-only key cannot escalate itself.
  • WorkOS marks revoked keys as invalid immediately. There is no cache window where a revoked key can still pass validation.

What the validation response gives you

The object returned from validation contains everything your application needs to both authenticate and authorize the request in a single step:

  
{
  "id": "api_key_01HXYZ...",
  "name": "CI pipeline key",
  "organizationId": "org_01HXYZ...",
  "permissions": ["projects:read", "tasks:read"],
  "createdAt": "2026-04-01T10:00:00.000Z",
  "lastUsedAt": "2026-05-18T14:32:11.000Z"
}
  

The lastUsedAt field is particularly useful if you want to surface usage information back to your users in the settings UI, or to build logic that flags keys that have not been used in a while.

Testing with real keys

Once your widget is rendering and your routes are protected, it is worth doing a quick end-to-end test before shipping.

Create a key from your settings page with only the projects:read permission. Copy the value at creation. Then make a request against your API:

  
curl https://yourapp.com/api/projects \
  -H "Authorization: Bearer sk_live_..."
  

You should get a successful response with project data. Try the same key against a write endpoint:

  
curl https://yourapp.com/api/projects \
  -X POST \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "New project"}'
  

That should return a 403, since the key does not carry projects:write. Go back to the settings page, revoke the key, and repeat the first request. You should now get a 401, confirming that revocation takes effect immediately.

Audit trail

Every key lifecycle event is tracked automatically. WorkOS emits api_key.created and api_key.revoked events whenever a key is created or revoked, by any path (the widget, the API, or an admin acting in the dashboard).

You can view these events in the Events section of your WorkOS dashboard, or subscribe to them programmatically via the events API to pipe them into your own audit log or alerting system. This gives you a clean record of when keys were created, by whom, and when they were revoked, without any custom instrumentation on your side.

Giving admins visibility

Admins with the widgets:user-api-keys:manage-all permission can see all active API keys across the organization, not just their own. This gives your customers' IT and security teams a way to audit what keys are in circulation and revoke anything that looks wrong, even if the original creator is no longer with the company.

You can also surface this from your own dashboard by hitting the List API keys endpoint server-side and building your own admin UI on top of the data, if you want more control over the presentation.

Putting it together

To recap what we have covered:

  • Configure available permissions for API keys under Authorization > Configuration in the WorkOS dashboard
  • For org-scoped keys, assign widgets:api-keys:manage to admin roles. For user-scoped keys, assign widgets:user-api-keys:manage-self to user roles and widgets:user-api-keys:manage-all to admin roles
  • Drop the <ApiKeys /> widget into your settings page with either scope="user" or scope="organization" depending on the key type
  • Use validateApiKey (Next.js) or workos.apiKeys.validateApiKey (Node/Express) to verify keys on your backend routes
  • Check the permissions array on the validated key to enforce route-level authorization
  • Use the WorkOS events API to subscribe to api_key.created and api_key.revoked for your audit log

The full API reference is at workos.com/docs/authkit/api-keys, and if you are also thinking about how agent credentials fit into this picture, the machine identity guide covers when to reach for user-scoped keys, org-scoped keys, and M2M Applications depending on what the agent is actually doing.