Skip to main content

    API Reference

    Base URL: https://api.behest.app

    All request and response bodies are JSON. All endpoints that require authentication accept a Bearer token in the Authorization header.

    Authentication Methods

    MethodHeaderUsed by
    Dashboard sessionAuthorization: Bearer <session-token>Dashboard BFF (Next.js API routes proxy to behest-auth using a service JWT)
    API keyAuthorization: Bearer behest_sk_live_...POST /auth/v1/auth/mint only
    Admin secretX-Admin-Secret: <secret>Admin kill-switch and admin provider endpoints

    Routes marked dashboard-only require a dashboard session token — they are not intended for direct use by end-user applications.


    Auth

    POST /auth/v1/auth/mint

    Exchange an API key for a short-lived RS256 JWT.

    Authentication: API key in Authorization: Bearer header.

    Request body:

    typescript
    {
      user_id: string;   // required, 1-255 chars, not a reserved identifier
      role?: "user" | "dashboard-service" | "admin";  // default: role on the API key
      ttl?: number;      // seconds, 60-86400, default 3600
      tier?: string;     // optional tier name; injected as a JWT claim
    }

    Response 200:

    typescript
    {
      access_token: string; // RS256 JWT
      token_type: "Bearer";
      project_id: string; // UUID
      expires_in: number; // seconds
    }

    Errors:

    StatusCondition
    400Missing or invalid user_id, invalid role, ttl out of range, reserved user_id
    401API key missing, invalid format, not found, revoked, or project suspended
    bash
    curl -X POST https://api.behest.app/auth/v1/auth/mint \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer behest_sk_live_abc123..." \
      -d '{"user_id": "user-123", "ttl": 3600}'
    typescript
    const resp = await fetch("https://api.behest.app/auth/v1/auth/mint", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({ user_id: userId, ttl: 3600 }),
    });
    const { access_token, expires_in } = await resp.json();

    GET /.well-known/jwks.json

    Return the RSA public key used to sign Behest JWTs in JWK Set format.

    Authentication: None.

    Response 200:

    typescript
    {
      keys: Array<{
        kty: "RSA";
        n: string;
        e: string;
        kid: string;
        use: "sig";
        alg: "RS256";
      }>;
    }
    bash
    curl https://api.behest.app/.well-known/jwks.json

    Tenants

    POST /auth/v1/tenants

    Create a new tenant. Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      name: string; // required
    }

    Response 201:

    typescript
    {
      id: string; // UUID
      name: string;
    }

    Errors: 400 if name is missing.

    bash
    curl -X POST https://api.behest.app/auth/v1/tenants \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer <session-token>" \
      -d '{"name": "Acme Corp"}'

    Projects

    GET /auth/v1/tenants/:tenantId/projects

    List all projects for a tenant. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    Array<{
      id: string;
      tenant_id: string;
      name: string;
      slug: string;
      status: string; // "active" | "suspended"
      dns_status: string; // "PENDING" | "READY" | "FAILED"
      created_at: string; // ISO 8601
    }>;
    bash
    curl https://api.behest.app/auth/v1/tenants/<tenantId>/projects \
      -H "Authorization: Bearer <session-token>"

    POST /auth/v1/tenants/:tenantId/projects

    Create a new project. Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      name: string; // required
    }

    Response 201:

    typescript
    {
      id: string;
      tenant_id: string;
      name: string;
      slug: string; // e.g. "amber-fox-042" — auto-generated
      fqdn_dev: string; // "{slug}.dev.internal.behest.ai"
      fqdn_prod: string; // "{slug}.behest.app"
      dns_status: "PENDING";
    }

    Errors:

    StatusCondition
    400name is missing
    403Free plan tenant already has 3 active projects
    404Tenant not found

    Side effects: Enqueues a DNS provisioning job for the project FQDN. Creates a project_settings row with defaults.

    bash
    curl -X POST https://api.behest.app/auth/v1/tenants/<tenantId>/projects \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer <session-token>" \
      -d '{"name": "Support Chatbot"}'

    GET /auth/v1/projects/:projectId/provisioning

    Get DNS provisioning status for a project. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      dns_status: "PENDING" | "READY" | "FAILED";
      dns_last_error: string | null;
      fqdn_dev: string;
      fqdn_prod: string;
      slug: string;
      dns_updated_at: string | null; // ISO 8601
    }

    POST /auth/v1/projects/:projectId/provisioning/retry

    Retry DNS provisioning after a failure. Dashboard-only. Only valid when dns_status is "FAILED".

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      dns_status: "PENDING";
    }

    Errors: 400 if dns_status is not "FAILED".


    POST /auth/v1/projects/:id/suspend

    Suspend a project. Revokes all active API keys and removes DNS records. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      status: "suspended";
    }

    Idempotent — suspending an already-suspended project returns 200 without side effects.


    Project Settings

    GET /auth/v1/projects/:projectId/settings

    Get all settings for a project. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      id: string;
      project_id: string;
      system_prompt: string | null;
      memory_window: number;               // 0-500, default 50
      cors_origins: string[];
      cors_allow_credentials: boolean;
      rpm_limit: number;                   // 1-10000, default 60
      tokens_per_day: number;              // min 1000, default 1000000
      pii_mode: "disabled" | "shadow" | "enforce";
      pii_entities: Record<string, "MASK" | "REDACT" | "BLOCK">;
      sentinel_mode: "disabled" | "shadow" | "enforce";
      sentinel_blocklist: string[];        // max 200 terms
      memory_enabled: boolean;
      retention_days: number | null;       // 1-365
      store_tool_calls: boolean;
      provider_model: string | null;       // deployed model
      draft_provider_model: string | null; // unsaved model draft
      draft_saved_at: string | null;
      deployed_at: string | null;
      created_at: string;
      updated_at: string;
    }

    PUT /auth/v1/projects/:projectId/settings

    Update one or more project settings. Changes are saved to PostgreSQL as a draft — they do not affect live traffic until you call the deploy endpoint. Dashboard-only.

    Authentication: Dashboard session.

    Request body (all fields optional — send only what you want to change):

    typescript
    {
      system_prompt?: string;          // max 32000 chars
      memory_window?: number;          // 0-500
      cors_origins?: string[];         // each must be "scheme://host" with no trailing slash, or "*"
      cors_allow_credentials?: boolean; // cannot be true when cors_origins includes "*"
      rpm_limit?: number;              // 1-10000
      tokens_per_day?: number;         // >= 1000
      pii_mode?: "disabled" | "shadow" | "enforce";
      pii_entities?: Record<string, "MASK" | "REDACT" | "BLOCK">;
      sentinel_mode?: "disabled" | "shadow" | "enforce";
      sentinel_blocklist?: string[];   // max 200 terms
      memory_enabled?: boolean;
      retention_days?: number | null;  // 1-365 or null
      store_tool_calls?: boolean;
      provider_model?: string | null;  // known model ID or null; saved as draft
    }

    Response 200: Updated settings row (same shape as GET response).

    Errors: 400 for validation failures per field. 404 if settings not found.


    POST /auth/v1/projects/:projectId/settings/deploy

    Push draft settings to live traffic. Updates Redis so Kong picks up the changes immediately. Dashboard-only.

    Authentication: Dashboard session.

    No request body.

    Response 200:

    typescript
    {
      deployed: true;
      project_id: string;
      deployed_at: string; // ISO 8601
    }

    Errors:

    StatusCondition
    404Settings not found
    502Settings saved to DB but Redis sync failed; retry the deploy

    POST /auth/v1/projects/:projectId/settings/discard-draft

    Revert project settings DB row to the last deployed snapshot (read from Redis). Dashboard-only.

    Authentication: Dashboard session.

    No request body.

    Response 200: Reverted settings row.

    Errors:

    StatusCodeCondition
    409NO_DEPLOYED_SNAPSHOTNo deploy has ever been run — nothing to revert to

    Project Model Selection

    PUT /auth/v1/projects/:projectId/settings/model

    Set the model for a project. Takes effect immediately (syncs to Redis without a deploy step). Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      provider_model: string; // required, must be a known model ID
    }

    Response 200:

    typescript
    {
      configured: true;
      provider_model: string;
      provider_type: string; // e.g. "openai", "anthropic"
    }

    Errors:

    StatusCodeCondition
    400MISSING_MODELprovider_model is absent or empty
    400UNKNOWN_MODELModel ID not in the known model registry
    404Project not found
    422PROVIDER_NOT_CONFIGUREDModel belongs to a provider for which no BYOK key is configured

    DELETE /auth/v1/projects/:projectId/settings/model

    Reset the project to the platform default model. Dashboard-only.

    Authentication: Dashboard session.

    Response 204: No body.

    Errors: 404 if no model is configured.


    PUT /auth/v1/projects/:projectId/settings/model/draft

    Auto-save a model selection as a draft (DB only, does not affect live traffic). Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      provider_model: string;
    }

    Response 200:

    typescript
    {
      draft_provider_model: string | null;
      draft_saved_at: string | null;
    }

    If the draft model matches the currently deployed model, the draft is cleared and draft_provider_model is returned as null.


    DELETE /auth/v1/projects/:projectId/settings/model/draft

    Discard the current model draft. Dashboard-only.

    Authentication: Dashboard session.

    Response 204: No body.


    POST /auth/v1/projects/:projectId/settings/model/publish

    Publish a saved model draft to live traffic. Equivalent to deploy but scoped to model selection. Dashboard-only.

    Authentication: Dashboard session.

    No request body.

    Response 200:

    typescript
    {
      provider_model: string;
      deployed_at: string;
    }

    Errors:

    StatusCodeCondition
    409NO_DRAFTNo draft exists to publish
    422PROVIDER_NOT_CONFIGUREDBYOK key for the draft model's provider was removed since the draft was saved

    API Keys

    GET /auth/v1/projects/:projectId/api-keys

    List all active (non-revoked) API keys for a project. Plaintext keys are never returned. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    Array<{
      id: string;
      name: string | null;
      role: string;
      prefix: string; // first 8 chars of the key's SHA-256 lookup hash + "…"
      createdAt: string; // ISO 8601
    }>;

    POST /auth/v1/projects/:projectId/api-keys

    Create a new API key for a project. The plaintext key is returned exactly once. Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      name?: string;  // default "default"
    }

    Response 201:

    typescript
    {
      id: string;
      project_id: string;
      api_key: string; // "behest_sk_live_..." — shown once only
      message: string; // "Store this key securely. It will not be shown again."
    }
    bash
    curl -X POST https://api.behest.app/auth/v1/projects/<projectId>/api-keys \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer <session-token>" \
      -d '{"name": "production"}'

    POST /auth/v1/projects/:projectId/api-keys/:keyId/rotate

    Rotate an API key — atomically revoke the current key and issue a new one. The new plaintext key is returned once. Dashboard-only.

    Authentication: Dashboard session.

    No request body.

    Response 200:

    typescript
    {
      id: string;
      api_key: string; // new plaintext key — shown once only
      message: "New key generated. Old key is revoked.";
    }

    Errors: 404 if key not found or already revoked.


    POST /auth/v1/projects/:projectId/api-keys/:keyId/revoke

    Permanently revoke an API key. Cannot be undone. Dashboard-only.

    Authentication: Dashboard session.

    No request body.

    Response 200:

    typescript
    {
      message: "API key revoked";
    }

    Errors: 404 if key not found.


    POST /auth/v1/api-keys/:keyId/rotate

    Simplified rotate — no project ID required in the path. Dashboard-only.

    Authentication: Dashboard session.

    Response 200: Same as /v1/projects/:projectId/api-keys/:keyId/rotate.


    DELETE /auth/v1/api-keys/:keyId

    Simplified revoke — no project ID required in the path. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      message: "API key revoked";
    }

    Providers (BYOK)

    GET /auth/v1/tenants/:tenantId/providers

    List configured provider keys for a tenant. Never returns ciphertext or plaintext keys. Dashboard-only.

    Authentication: Dashboard session.

    Query params:

    • ?fields=types — lightweight response returning only a list of configured provider type names (reads from Redis only)

    Response 200 (full):

    typescript
    {
      providers: Array<{
        provider_type: string; // "openai" | "anthropic" | "google" | "openrouter" | "mistral" | "cohere"
        key_last4: string; // last 4 chars of the original key
        key_set_at: string; // ISO 8601
        projects_using_count: number; // how many projects in this tenant use this provider's models
      }>;
    }

    Response 200 (?fields=types):

    typescript
    {
      provider_types: string[];  // e.g. ["openai", "anthropic"]
    }

    PUT /auth/v1/tenants/:tenantId/providers/:providerType

    Add or replace a provider API key. Validates the key against the provider's API before storing. Dashboard-only.

    Authentication: Dashboard session.

    Supported providerType values: openai, anthropic, google, openrouter, mistral, cohere

    Request body:

    typescript
    {
      api_key: string;           // required — the provider API key
      base_url_override?: string; // optional custom API base URL (SSRF-safe validation applied)
    }

    Response 200:

    typescript
    {
      configured: true;
      provider_type: string;
      key_last4: string;
      key_set_at: string; // ISO 8601
    }

    Errors:

    StatusCodeCondition
    400UNSUPPORTED_PROVIDERUnknown providerType
    400INVALID_KEY_FORMATKey does not match provider's expected format
    400INVALID_BASE_URLbase_url_override fails SSRF safety check
    403BYOAK_REQUIRES_PROTenant is on the free plan
    404TENANT_NOT_FOUNDTenant not found
    422KEY_VALIDATION_FAILEDKey was rejected by the provider API
    429Too many requests (internal)

    Note: There is a minimum 500ms response time for this endpoint to mitigate timing attacks on key validation.

    bash
    curl -X PUT https://api.behest.app/auth/v1/tenants/<tenantId>/providers/openai \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer <session-token>" \
      -d '{"api_key": "sk-proj-abc123..."}'

    DELETE /auth/v1/tenants/:tenantId/providers/:providerType

    Remove a provider key. Takes effect immediately (Redis cleanup is synchronous). Dashboard-only.

    Authentication: Dashboard session.

    Response 204: No body.

    Errors: 404 if no key is configured for this provider.


    POST /auth/v1/tenants/:tenantId/providers/:providerType/validate

    Validate a provider key without storing it. Rate-limited to 5 requests per minute per tenant. Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      api_key: string;
    }

    Response 200:

    typescript
    {
      valid: boolean;
      reason?: "invalid_key" | "network_error" | "timeout" | "rate_limited";
    }

    Errors: 429 if rate limit exceeded (5/minute).


    GET /auth/v1/tenants/:tenantId/providers/models

    List all models available to the tenant (platform models + models from configured BYOK providers). Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      models: Array<{
        modelId: string;
        displayName: string;
        providerSlug: string;
        providerDisplayName: string;
        capabilities: Record<string, unknown>;
        contextWindow: number | null;
        maxOutputTokens: number | null;
        supportsStreaming: boolean | null;
        supportsToolUse: boolean | null;
        supportsVision: boolean | null;
      }>;
    }

    Project Tiers

    Tiers define per-segment overrides for a project's settings. Up to 3 tiers per project.

    GET /auth/v1/projects/:projectId/tiers

    List all tiers with their resolved (merged) settings. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    Array<{
      id: string;
      project_id: string;
      name: string;
      sort_order: number;
      overrides: Record<string, unknown>;
      resolved: Record<string, unknown>; // project defaults merged with tier overrides
      created_at: string;
      updated_at: string;
    }>;

    POST /auth/v1/projects/:projectId/tiers

    Create a tier. Dashboard-only.

    Authentication: Dashboard session.

    Request body:

    typescript
    {
      name: string;                          // required, unique per project
      sort_order?: number;                   // default 0
      overrides?: {                          // all optional
        rpm_limit?: number;
        tokens_per_day?: number;
        pii_mode?: "disabled" | "shadow" | "enforce";
        pii_entities?: Record<string, "MASK" | "REDACT" | "BLOCK">;
        sentinel_mode?: "disabled" | "shadow" | "enforce";
        sentinel_blocklist?: string[];
        memory_enabled?: boolean;
        memory_window?: number;
        retention_days?: number | null;
        store_tool_calls?: boolean;
        provider_model?: string;
        system_prompt?: string;
      };
    }

    Response 201: Created tier (without resolved).

    Errors:

    StatusCondition
    400Validation failure
    403Caller does not own the project
    409Maximum 3 tiers already exist, or name already taken
    422overrides.provider_model references a provider with no BYOK key

    PUT /auth/v1/projects/:projectId/tiers/:tierId

    Update a tier. Dashboard-only.

    Authentication: Dashboard session.

    Request body: Same shape as POST, all fields optional.

    Response 200: Updated tier.


    DELETE /auth/v1/projects/:projectId/tiers/:tierId

    Delete a tier. Dashboard-only.

    Authentication: Dashboard session.

    Response 204: No body.


    GET /auth/v1/projects/:projectId/tiers/:tierId/resolved

    Get the fully resolved settings for a single tier (project defaults merged with tier overrides). Dashboard-only.

    Authentication: Dashboard session.

    Response 200: Object of resolved setting values.


    Memory Configuration

    GET /auth/v1/projects/:projectId/memory

    Get memory configuration for a project. Dashboard-only.

    Authentication: Dashboard session.

    Response 200:

    typescript
    {
      project_id: string;
      memory_enabled: boolean;
      memory_window: number;
      retention_days: number | null;
      system_prompt: string | null;
      store_tool_calls: boolean;
    }

    Playground

    POST /auth/v1/projects/:projectId/playground

    Test the project's draft model configuration against real LLM APIs. Rate-limited to 50 requests per hour per user per project. Dashboard-only.

    Authentication: Dashboard session.

    Request headers:

    • X-User-Id: optional user identifier for rate limiting (default: "anonymous")

    Request body:

    typescript
    {
      messages: Array<{ role: string; content: string }>;
    }

    Response: SSE stream (Content-Type: text/event-stream) forwarding the LiteLLM streaming response.

    Errors:

    StatusCondition
    400messages is missing or empty
    404No draft model configured
    422Provider key not configured
    429Rate limit exceeded (50/hour)

    Admin: Kill Switches

    All kill switch endpoints require X-Admin-Secret header matching the ADMIN_SECRET environment variable.

    POST /v1/admin/killswitch/global

    Enable or disable the global kill switch.

    Authentication: X-Admin-Secret header.

    Request body:

    typescript
    {
      enabled: boolean;
    }

    Response 200:

    typescript
    {
      killswitch: "global";
      enabled: boolean;
    }

    POST /v1/admin/killswitch/tenant/:tenantId

    Enable or disable a tenant-scoped kill switch.

    Authentication: X-Admin-Secret header.

    Request body: { enabled: boolean }

    Response 200:

    typescript
    {
      killswitch: "tenant";
      tenantId: string;
      enabled: boolean;
    }

    POST /v1/admin/killswitch/project/:projectId

    Enable or disable a project-scoped kill switch.

    Authentication: X-Admin-Secret header.

    Request body: { enabled: boolean }

    Response 200:

    typescript
    {
      killswitch: "project";
      projectId: string;
      enabled: boolean;
    }

    GET /v1/admin/killswitch/status

    Return the current kill switch state across all scopes.

    Authentication: X-Admin-Secret header.

    Response 200:

    typescript
    {
      global: boolean;
      tenants: string[];   // tenant IDs with kill switch enabled
      projects: string[];  // project IDs with kill switch enabled
    }

    Admin: Provider Catalog

    All routes under /auth/v1/admin/providers require the X-Admin-Secret header. These endpoints manage the global provider catalog (not tenant-specific keys).

    GET /auth/v1/admin/providers

    List all providers with model counts.

    Response 200:

    typescript
    {
      providers: Array<{
        id: string;
        name: string;
        slug: string;
        display_name: string;
        is_active: boolean;
        model_count: number;
        created_at: string;
        updated_at: string;
      }>;
    }

    POST /auth/v1/admin/providers

    Create a new provider in the catalog.

    Request body:

    typescript
    {
      name: string;                     // required, unique
      slug: string;                     // required, unique
      displayName: string;              // required
      modelsEndpointUrl?: string;
      modelsEndpointAuthType?: string;
      authConfigSchema?: object;
      supportedFeatures?: object;
    }

    POST /auth/v1/admin/providers/:id/sync-models

    Trigger model discovery for a provider — fetches the provider's models API and updates the catalog.

    No request body.


    Health

    Behest services expose standard health and readiness probes. These are not routed through Kong.

    EndpointServiceDescription
    GET /healthzAll servicesLiveness probe — returns 200 { status: "ok" } if the process is alive
    GET /readyzredis-sync-workerReadiness probe — returns 200 only when the worker has completed its first sync cycle

    Enterprise Token FinOps: Enforce hard budgets and attribute costs per session.

    Learn more