Better Auth in Rust

API Key

Create, manage, and validate API keys with rate limiting and usage tracking.

The ApiKeyPlugin provides API key management with support for rate limiting, usage quotas, expiration, and permissions.

Setup

These examples assume you've already defined AppAuthSchema. If you want a complete setup from entity definitions through Axum mounting, start with the Axum integration guide.

use better_auth::plugins::ApiKeyPlugin;

let auth = BetterAuth::<AppAuthSchema>::new(config)
    .store(store)
    .plugin(ApiKeyPlugin::builder().build())
    .build()
    .await?;

Configuration

use better_auth::plugins::api_key::{ApiKeyConfig, RateLimitDefaults, KeyExpirationConfig};

let auth = BetterAuth::<AppAuthSchema>::new(config)
    .store(store)
    .plugin(
        ApiKeyPlugin::builder()
            .key_length(64)
            .prefix("sk_".to_string())
            .enable_metadata(true)
            .build()
    )
    .build()
    .await?;
OptionTypeDefaultDescription
key_lengthusize64Random characters excluding the prefix; 0 selects the default length of 64, matching upstream
prefixOption<String>NoneGlobal prefix for all generated keys
enable_metadataboolfalseAllow storing metadata on keys
require_nameboolfalseRequire a name when creating keys
default_permissionsOption<HashMap<String, Vec<String>>>NonePermissions assigned when creation omits permissions
api_key_headersVec<String>["x-api-key"]Headers checked when API key sessions are enabled
enable_session_for_api_keysboolfalseEmulate sessions for API-key-authenticated requests
config_idString"default"Name of this configuration, stored on every key it creates
referencesApiKeyReferencesUserWhether keys belong to the signed-in user or to an organization

Named Configurations and Organization-Owned Keys

A plugin instance holds one or more configurations. Each is identified by its config_id, and clients select one by sending configId. Creation, lookup, update, and deletion use the "default" configuration when configId is absent. Listing without configId includes all registered configurations for the requested owner type. Register additional configurations with configuration:

use better_auth::plugins::api_key::{ApiKeyConfig, ApiKeyReferences};

let auth = BetterAuth::<AppAuthSchema>::new(config)
    .store(store)
    .plugin(OrganizationPlugin::new())
    .plugin(
        ApiKeyPlugin::builder()
            .prefix("sk_".to_string())
            .build()
            .configuration(ApiKeyConfig {
                config_id: "org".to_string(),
                references: ApiKeyReferences::Organization,
                prefix: Some("org_".to_string()),
                ..Default::default()
            }),
    )
    .build()
    .await?;

When a configuration sets references: ApiKeyReferences::Organization, its keys belong to an organization instead of a user:

  • create and list require an organizationId (body field or query parameter respectively). get, update, and delete resolve the organization from the key itself.
  • The caller must be a member of that organization and hold the matching apikey action (create, read, update, or delete) in the organization plugin's RBAC. The creator role is allowed every action. The default roles grant none, so applications grant them through custom roles; see Organization RBAC.
  • The organization plugin must be registered, or requests fail with ORGANIZATION_PLUGIN_REQUIRED.

Every key carries referenceId (the owning user or organization id) and configId in its response.

How It Works

Key Generation

API keys are generated as random alphabetic strings ([a-zA-Z]) of the configured length. The key format is:

{prefix}{random_alpha_chars}

Only the SHA-256 hash of the key is stored in the database. The full key is returned only once during creation and cannot be retrieved later.

Key Structure

Each API key has:

  • start: First 6 characters of the full key including prefix (for UI identification)
  • prefix: Optional prefix (e.g., sk_, pk_)
  • enabled: Whether the key is active
  • remaining: Usage quota (decremented on each use, null = unlimited)
  • referenceId: Owner of the key: a user id, or an organization id for organization-owned configurations
  • configId: The configuration the key was created under
  • expiresAt: Optional expiration date (set via expiresIn in seconds)
  • Rate limiting: Counter-based rate limits with configurable time window and max requests

API Endpoints

The API Key plugin exposes 5 client-facing endpoints. All require an authenticated session.

EndpointMethodDescription
/api-key/createPOSTCreate a new API key (returns key only once)
/api-key/getGETGet key metadata by ID (query param id)
/api-key/listGETList keys for the authenticated user or an organization (paginated)
/api-key/updatePOSTUpdate key settings (body field keyId)
/api-key/deletePOSTDelete an API key (body field keyId)

Listing

GET /api-key/list returns a paginated envelope rather than a bare array:

{
  "apiKeys": [ ... ],
  "total": 42,
  "limit": 20,
  "offset": 0
}
Query parameterDescription
configIdConfiguration to list keys from; absent lists all registered configurations for the requested owner type
organizationIdSelects organization-owned keys; omit to list user-owned keys
limit, offsetPage window; total counts the whole result set and both fields are omitted when not sent
sortByOne of createdAt, updatedAt, name, expiresAt
sortDirectionasc (default) or desc

The full key value is returned only during creation. Store it securely — it cannot be retrieved later.

Fields like permissions, remaining, refillAmount, refillInterval, rateLimitEnabled, rateLimitTimeWindow, and rateLimitMax are server-only and cannot be set from HTTP client requests.

Server-only Operations

Use auth.api_keys()? to access the registered plugin and initialized context from trusted Rust code. The accessor returns a configuration error when ApiKeyPlugin is not registered.

use std::collections::HashMap;
use better_auth::plugins::ApiKeyPlugin;
use better_auth::server_api::{
    CreateKeyOptions, RequiredActions, UpdateKeyOptions, VerifyKeyOptions,
};

let auth = BetterAuth::<AppAuthSchema>::new(config)
    .store(store)
    .plugin(ApiKeyPlugin::builder().build())
    .build()
    .await?;
let api_keys = auth.api_keys()?;

let issued = api_keys.create(user_id, CreateKeyOptions {
    name: Some("worker".to_owned()),
    permissions: Some(HashMap::from([
        ("nodes".to_owned(), vec!["heartbeat".to_owned()]),
    ])),
    ..Default::default()
}).await?;

let key = api_keys.verify(&issued.key, VerifyKeyOptions {
    config_id: Some("default".to_owned()),
    permissions: Some(HashMap::from([
        ("nodes".to_owned(), RequiredActions::All(vec!["heartbeat".to_owned()])),
    ])),
}).await?;

api_keys.update(user_id, &key.id, UpdateKeyOptions {
    enabled: Some(false),
    ..Default::default()
}).await?;

Creation and update require the acting user's ID as a method argument. Organization configurations also require organization_id during creation and check the acting user's organization permissions. Verification requires the key, without a user session. No public HTTP verification endpoint is exposed.

Use RequiredActions::All for upstream AND semantics or RequiredActions::Any for OR semantics within a resource. Every requested resource must pass. Numeric options use f64 to preserve upstream fractional values.

Use FieldUpdate::Unchanged, FieldUpdate::Clear, or FieldUpdate::Set(value) for nullable updates to permissions, metadata, and expires_in. Other omitted update fields remain unchanged. For example:

use better_auth::server_api::FieldUpdate;

api_keys.update(user_id, &key.id, UpdateKeyOptions {
    expires_in: FieldUpdate::Clear,
    ..Default::default()
}).await?;

The lower-level ApiKeyPlugin::create_key, update_key, and verify_api_key methods remain available for callers that already manage an AuthContext and wire request DTOs.

Verification checks configuration, expiration, permissions, quota, and rate limits. A permission mismatch does not consume quota. To match Better Auth 1.7.6, a rate-limit rejection consumes quota before returning RATE_LIMITED; details.tryAgainIn uses milliseconds. Expired keys and exhausted keys without a refill amount are deleted during verification.

Handle ApiKeyVerificationError::Validation as a rejected credential and propagate ApiKeyVerificationError::Internal as an infrastructure failure. The successful result includes reference_id, config_id, permissions, and updated counters. It omits the plaintext key and stored hash.

Pass config_id when a route accepts credentials from one configuration. Without that field, lookup uses the default configuration's hashing setting, then validates against the configuration that issued the key.

Session Emulation

Enable enable_session_for_api_keys to authenticate user-owned keys through the configured headers. The plugin provides a virtual session to protected routes without creating a database session. Its hook answers both GET and POST requests to /get-session, matching upstream behavior. Organization-owned keys cannot create user sessions. Session emulation grants access as the key owner; it does not enforce the key’s resource permissions on each route. For scoped machine credentials, keep session emulation disabled and pass the required permissions to server-side verification.

Numeric Storage

Better Auth accepts fractional quota, refill, and rate-limit values. Rust uses f64 for those fields and request_count. The bundled migration converts existing PostgreSQL columns to double precision and rebuilds SQLite API key tables with real-valued columns, preserving existing keys. Applications that own their migrations must update the corresponding columns and model fields.

Errors

StatusCondition
400Missing or invalid required fields
400ORGANIZATION_ID_REQUIRED
403USER_NOT_MEMBER_OF_ORGANIZATION or INSUFFICIENT_API_KEY_PERMISSIONS
500ORGANIZATION_PLUGIN_REQUIRED
401Not authenticated
404API key not found, not owned by the caller, or not in the addressed configuration

On this page