How Credential Refresh Works for API Sources with Renew Endpoints

Credential refresh for API sources with renew endpoints works by detecting sources configured with a renewEndpoint, automatically building HTTP requests to that endpoint with the current token, parsing the response for new tokens and expiry times, and updating the credential store without requiring OAuth refresh tokens.

The craft-agents-oss repository implements a generic mechanism that allows API sources to maintain valid access tokens through custom HTTP renew endpoints. Unlike traditional OAuth refresh flows, this system supports any bearer-token API by interpreting a declarative renewEndpoint configuration. Understanding how credential refresh works for API sources with renew endpoints ensures your agents maintain uninterrupted access to external services even when initial tokens expire.

Detecting Refreshable API Sources

The system identifies a source as refreshable when its configuration type is api and it includes a renewEndpoint definition. The helper function sourceHasRenewEndpoint in packages/shared/src/sources/types.ts (lines 221-229) performs this validation, returning true only when both conditions are met. This check enables the TokenRefreshManager to distinguish between static credentials and those requiring periodic renewal.

Building the Renew HTTP Request

When a token expires, the CredentialManager constructs an HTTP request using the ApiRenewEndpoint configuration defined in packages/shared/src/sources/types.ts (lines 338-379). The build process follows a specific hierarchy:

  • URL – Combines the source baseUrl with the renewEndpoint.path, or uses an absolute URL if provided.
  • Method – Defaults to POST unless explicitly overridden in the configuration.
  • Headers – Merges the source's default headers with renewEndpoint.headers, then automatically injects Authorization: Bearer <current-token> if not already present.
  • Body – JSON-stringifies the request body after substituting {{token}} placeholders with the current access token.

This logic resides in packages/shared/src/sources/credential-manager.ts between lines 992 and 1034.

Parsing the Response and Updating Stored Credentials

After executing the renew request, the credential manager processes the JSON response to extract authentication data. It reads the tokenField (defaulting to access_token) for the new token and the expiresInField (defaulting to expires_in) to calculate the expiration timestamp as expiresAt = now + ttl * 1000. If the response omits the expiry field, the optional fallbackTtlSecs value provides a safety net. The implementation in packages/shared/src/sources/credential-manager.ts (lines 1034-1047) handles this parsing, while line 1059 persists the updated token and expiry to the credential store.

Orchestrating Periodic Refreshes

The TokenRefreshManager coordinates automatic refreshes by periodically invoking ensureFreshToken for all sources flagged as OAuth-based or containing a renewEndpoint. Located at line 251 of packages/shared/src/sources/token-refresh-manager.ts, this method calls the token getter generated by the credential manager for each source, ensuring tokens remain valid before they expire. If a renew request fails due to network errors, non-2xx responses, or missing token fields, the manager throws an exception, allowing the refresh loop to retry during the next cycle.

Configuration and Code Examples

Defining a Source with a Renew Endpoint

// examples/src/sources.ts
import { SourceConfig } from '@craft-agents/shared/sources';

export const myApiSource: SourceConfig = {
  slug: 'my-api',
  type: 'api',
  baseUrl: 'https://api.example.com',
  defaultHeaders: { 'Accept': 'application/json' },
  api: {
    token: { /* initial token obtained elsewhere */ },
    renewEndpoint: {
      path: '/auth/refresh',          // relative to baseUrl
      method: 'POST',
      body: { grant_type: 'refresh_token', token: '{{token}}' },
      tokenField: 'access_token',
      expiresInField: 'expires_in',
      fallbackTtlSecs: 3600,
    },
  },
};

Source file: packages/shared/src/sources/types.ts – defines ApiRenewEndpoint and SourceConfig (see lines 338-379).

Manually Triggering a Token Refresh

import { CredentialManager } from '@craft-agents/shared/sources';

// Assume `source` is the config above
const credMgr = new CredentialManager();
await credMgr.refreshToken(source);   // triggers the renew request
const freshToken = await credMgr.getToken(source);
console.log('New token →', freshToken);

Implementation: CredentialManager.refreshToken (see credential-manager.ts – lines 985-1059).

Automatic Refresh in the Server

import { TokenRefreshManager } from '@craft-agents/shared/sources';

const manager = new TokenRefreshManager();
await manager.ensureFreshToken([myApiSource]); // called periodically

Implementation: TokenRefreshManager.ensureFreshToken (see token-refresh-manager.ts – line 251).

Summary

  • API sources with a renewEndpoint configuration are detected as refreshable by the sourceHasRenewEndpoint utility in packages/shared/src/sources/types.ts.
  • The CredentialManager builds HTTP requests by merging headers, substituting {{token}} placeholders, and auto-injecting Bearer authorization.
  • Responses are parsed for access_token and expires_in fields (with configurable defaults), calculating expiry timestamps using fallbackTtlSecs when necessary.
  • TokenRefreshManager orchestrates periodic validation through ensureFreshToken, refreshing credentials proactively without OAuth refresh tokens.

Frequently Asked Questions

What makes an API source eligible for credential refresh?

An API source becomes eligible when its type is set to api and it defines a renewEndpoint object in its configuration. The sourceHasRenewEndpoint function in packages/shared/src/sources/types.ts validates this by checking for the presence of the endpoint definition.

How does the system handle token substitution in renew requests?

The credential manager automatically replaces any {{token}} string found in the renewEndpoint.headers or body values with the current access token before sending the request. This substitution occurs in packages/shared/src/sources/credential-manager.ts alongside the automatic injection of the Authorization: Bearer header.

What happens if the renew endpoint response doesn't include an expiry time?

When the response lacks the configured expiresInField (default expires_in), the system falls back to the fallbackTtlSecs value specified in the renewEndpoint configuration. This ensures the token still receives a calculated expiration timestamp even when the API omits TTL information.

Can I manually trigger a credential refresh for debugging?

Yes, you can instantiate the CredentialManager and call await credMgr.refreshToken(source) to trigger the renew flow immediately. This executes the same logic used by the automatic refresh scheduler, including request building, token substitution, and credential storage updates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →