Authentication & Rate Limiting
API key management, authentication flow, rate limiting tiers, and security best practices for GridWonk API v1.
Public plugin authentication
When enabled, the public GridWonk plugin uses user OAuth followed by an exact-session workspace confirmation in the website. Its stable customer catalog and current membership checks are separate from the organization credentials documented below. A public-plugin bearer token cannot act as a first-party browser session for setup, legal acceptance, purchases, or human decisions. API keys and machine tokens cannot substitute for that browser confirmation.
Reviewed public help is readable without sign-in. Internal documentation and complete docs-host discovery require native internal authorization; merely having an approved customer session does not grant access.
Authentication & Rate Limiting
Existing organization API integrations use the following authentication lanes. Check the endpoint reference before choosing credentials; there is no single credential accepted by every route.
Each active request re-evaluates the organization's current API/MCP policy, billing state, and the credential's scope. API access, MCP access, and self-service key creation are separate controls; creating a key or connecting an MCP client does not preserve access after an administrator changes the applicable policy.
| Lane | Credential | Used for |
|---|---|---|
| User session | Approved first-party GridWonk browser session | Approved bootstrap and session-only routes |
| Client credentials | API key supplied to POST /api/v1/oauth/token | Exchange for a short-lived OAuth JSON Web Token (JWT) |
| Runtime Bearer | Organization API key or OAuth JWT | Most organization-scoped API v1 runtime routes |
Request parsing is bounded in every authentication lane. The OAuth token exchange accepts a body of at most 32 KiB, general API JSON defaults to a 1 MiB limit, and oversized or malformed input is rejected before route work begins. Request-provided size metadata is never used as the upload byte count.
API v1 authentication lanes
Authentication depends on the endpoint. A user session bootstraps approved access, client credentials exchange a key for a short-lived token, and runtime routes accept the credential types declared by their auth wrapper.
User session
Bootstrap and session-only routes
An approved first-party browser session reaches authorized bootstrap and session-only routes; internal docs require separate internal authority.
Client credentials
Token exchange
A client sends its API key to the OAuth token route and receives a bounded bearer JWT.
Runtime bearer
Organization API key
Most runtime routes accept an active allowed key and apply its organization, role, endpoint, rate, and cost limits.
Runtime bearer
OAuth JWT
Most routes protected by the shared API authentication layer also accept the short-lived OAuth token.
Source anchors: lib/auth/api-auth.ts, lib/auth/api-key-auth.ts, app/api/v1/oauth/token/route.ts, app/api/v1/auth/bootstrap-key/route.ts
API Key Format
gw_{environment}_{32-random-bytes-hex}- Live keys (
gw_live_...): Production environment, real data. - Test keys (
gw_test_...): Test-labeled API traffic. They do not, by themselves, create a separate data sandbox.
Total length: 72 characters.
Legacy gb_live_/gb_test_ keys are no longer accepted. Use a gw_live_... or gw_test_... key.
Using Your API Key
Include the key in the Authorization header on runtime requests that support API-key authentication:
curl -X GET https://gridwonk.com/api/v1/jobs \
-H "Authorization: Bearer gw_live_..."OAuth 2.0 Client Credentials
For integrations that prefer standard OAuth 2.0 flows, GridWonk supports the client_credentials grant type. This exchanges your API key for a short-lived JWT bearer token.
The machine-token issuer advertises the client_credentials grant. The separately gated public
plugin uses the configured user-OAuth issuer and browser sign-in. MCP clients must follow the exact
protected-resource metadata for their connection; the machine issuer is not a user sign-in endpoint.
Token Exchange
POST /api/v1/oauth/token
No authentication header required — credentials are sent in the request body.
Request (form-urlencoded):
curl -X POST https://gridwonk.com/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=gw_live_..." \
-d "client_secret=$GRIDBOOST_API_KEY"Runnable request (JSON):
curl -X POST https://gridwonk.com/api/v1/oauth/token \
-H "Content-Type: application/json" \
-d "{\"grant_type\":\"client_credentials\",\"client_secret\":\"$GRIDBOOST_API_KEY\"}"Request (HTTP Basic Auth):
curl -X POST https://gridwonk.com/api/v1/oauth/token \
-u "gw_live_...:$GRIDBOOST_API_KEY" \
-d "grant_type=client_credentials"Token exchange is rate-limited separately: 30 requests per minute per client/network bucket and 10 requests per minute per client ID. Production counters use shared Redis state; forwarded headers are not accepted as a security identity unless a trusted edge header is configured. Do not treat an untrusted forwarded address as a credential or tenant identity.
OAuth failures always use the token endpoint's error and error_description fields. An
unexpected server fault returns 500 server_error with an X-Request-ID header for support; it
does not expose database, signing, or credential details.
Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}Using the JWT Token
curl -X GET https://gridwonk.com/api/v1/jobs \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."The JWT token is accepted like an API key on most runtime endpoints protected by the API authentication layer. It does not replace an approved user session on the session-only bootstrap routes. A scope-less token can fetch /api/v1/openapi.json; partner-scoped tokens remain confined to partner endpoints. The token-exchange route receives client credentials in its request rather than an access token.
Token Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
grant_type | string | Yes | — | Must be client_credentials |
client_id | string | No | — | API key prefix (for verification) |
client_secret | string | Yes | — | Full GridWonk API key |
scope | string | No | — | Space-delimited partner scopes to embed in the token (see below) |
resource | absolute URL | No | Legacy API audience | Bind the token to https://gridwonk.com/api/v1/mcp; admin MCP continues to require a browser admin session or raw internal management key, and other targets are rejected |
expires_in | integer | No | 3600 | Token lifetime in seconds (60–86,400) |
Partner Scopes
Partner packet REST endpoints (/api/v1/packets/*) and the packet tools on the
hosted MCP gateway (/api/v1/mcp) require an access token minted with an
explicit partner scope. Raw API keys and scope-less JWTs are refused on packet
REST endpoints with reason: "insufficient_scope"; on MCP they can discover
and call only the historical broad-key tools, never packet tools.
| Scope | Grants |
|---|---|
partner.read | Packet read endpoints (intake status, documents, sanitized text, review runs, workflow status, utility screening status) |
partner.write | Packet mutation endpoints (intake create/finalize, role confirm/correct, review start) |
Scopes are independent — partner.write does not imply partner.read; request
both if you need both:
curl -X POST https://gridwonk.com/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_secret=$GRIDBOOST_API_KEY" \
--data-urlencode "scope=partner.read partner.write"Utility application and notice list/detail reads use partner.read; utility screening starts,
manual freeze, adjudication, completion, study-requirement, and notice lifecycle writes use
partner.write. Current utility identity, membership, and application-state checks still
apply. Manual decisions and notice writes are not MCP tools even when a token has write scope.
See the utility lane guide.
Webhook subscriptions continue to use the management-key lane, not a partner token.
A partner-scoped token is strictly narrower than its key. Confinement is
two-way: packet REST endpoints refuse credentials without the required scopes,
and ordinary non-partner endpoints refuse partner-scoped tokens with
reason: "partner_token_confined". The hosted MCP gateway is the one
multiplexed exception: it accepts either credential lane, then filters both
tools/list and tools/call from the same per-tool policy. A partner.read
token sees only read-scoped packet tools; it cannot see broad tools, resources,
templates, prompts, or write-scoped packet tools. A broad key cannot see any
packet tool.
The API key creator's active organization membership is re-verified on every partner request. When an MCP session acts for a delegated client user, that user's membership and document visibility govern the packet call, and the membership is re-proved on every scoped session request. Stateless MCP calls re-verify the delegated identity on each call.
Partner-scoped tokens are capped at 1 hour regardless of the requested
expires_in (the scope lives only in the signed claim, so a short lifetime
bounds exposure; revoking the underlying API key invalidates outstanding
tokens immediately).
Requesting any scope outside this vocabulary fails the whole exchange with
invalid_scope — unknown scopes are never silently dropped. Two legacy
exceptions exist for backward compatibility: api:read and api:write (the
values this endpoint has always advertised in its response) are accepted as
no-ops and grant no partner scope.
Fine-grained customer scopes such as projects:read and all admin:* scopes are not live token
grants. They are neither advertised nor accepted until an interactive grant and consent flow can
issue and enforce them end to end.
When no partner scope is requested, the response advertises the legacy
"api:read api:write" value, which means "full authority of the underlying
API key" and is not a partner scope.
Token Properties
- Algorithm: HS256
- Default lifetime: 1 hour (3,600 seconds)
- Maximum lifetime: 24 hours (86,400 seconds)
- Minimum lifetime: 1 minute (60 seconds)
- Issuer:
gridboost-api - Audience:
gridboost-api-v1
When to Use OAuth vs API Keys
| Use Case | Recommended Auth |
|---|---|
| Server-to-server with long-running process | API key (simpler) |
| Short-lived automated tasks | JWT token (auto-expires) |
| Standard OAuth 2.0 compliance required | JWT token |
| PowerClerk or external platform integration | JWT token |
TypeScript Example
async function getGridWonkToken(apiKey: string): Promise<string> {
const res = await fetch('https://gridwonk.com/api/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_secret: apiKey,
}),
});
if (!res.ok) throw new Error(`OAuth error: ${res.status}`);
const { access_token } = await res.json();
return access_token;
}Python Example
import requests
def get_gridwonk_token(api_key: str) -> str:
resp = requests.post(
"https://gridwonk.com/api/v1/oauth/token",
data={
"grant_type": "client_credentials",
"client_secret": api_key,
},
)
resp.raise_for_status()
return resp.json()["access_token"]Key Properties
| Property | Description |
|---|---|
organizationId | All queries are scoped to this org (tenant isolation) |
environment | live or test |
keyRole | management or runtime |
status | active, suspended, or revoked |
rateLimitPerMinute | Default 60 requests/minute |
allowedEndpoints | Default ['/api/v1/*'] — can be restricted to specific routes |
monthlyCostLimitUsd | Optional spending ceiling |
expiresAt | Optional expiration timestamp |
Endpoint permissions are part of the API-key credential, not just a UI hint. Every API-key lane,
including internal management routes, checks the request path against allowedEndpoints before the
handler runs. Keep management keys narrowly scoped and mint a new key when an integration needs a
different route family.
Bootstrap Flow (Invite-Only Pilot)
For invite-only onboarding, the first management key is minted through session-auth endpoints:
GET /api/v1/auth/bootstrap/statusPOST /api/v1/auth/bootstrap-key
Eligibility states:
eligibleneeds_admin_membershipnot_allowlistedpayment_required
The status response also carries nextStep: create_bootstrap_key, request_org_admin_access,
add_payment_method, or contact_support. A not_allowlisted result caused by a platform API
access suspension or a billing suspension returns contact_support (the reason text names the
brake); every other not_allowlisted result returns request_org_admin_access.
Notes:
- Bootstrap requires direct active
owner/adminmembership in the target org. - Grant-derived access does not allow bootstrap.
- Bootstrap creates the first
managementkey.
Rate Limiting
Authentication throttles use shared Redis fixed-window state in production so instance scaling does
not split the budget. A deployment may add a client-IP dimension only through the explicitly
configured TRUSTED_CLIENT_IP_HEADER; caller-supplied X-Forwarded-For and X-Real-IP values are
not trusted.
Rate limits are enforced at two tiers:
| Tier | Window | Default Limit | Reset |
|---|---|---|---|
| Per-minute | Fixed current-minute window | 60 requests | End of current minute |
| Daily | Midnight UTC | 86,400 requests (60 × 1,440) | Next midnight UTC |
Rate Limit Headers
API-key-authenticated handler responses that pass auth, organization-profile, rate-limit, and organization-usage gates include:
X-RateLimit-Limit: 86400
X-RateLimit-Remaining: 85950
X-RateLimit-Reset: 1740009600
X-Request-Id: req_a1b2c3d4e5f67890| Header | Description |
|---|---|
X-RateLimit-Limit | Daily request cap |
X-RateLimit-Remaining | Requests remaining in current day |
X-RateLimit-Reset | Unix timestamp when daily counter resets (midnight UTC) |
X-Request-Id | Unique correlation ID for support and debugging |
Handling 429 Responses
When a rate limit is exceeded, the API returns:
{
"error": {
"code": "RATE_LIMITED",
"message": "Per-minute request limit exceeded",
"retryAfter": 42
},
"requestId": "req_a1b2c3d4e5f67890"
}Use the retryAfter field (seconds) to schedule your next request.
Organization API profile gates can deny otherwise-valid keys before route logic runs:
| Reason | Meaning |
|---|---|
api_access_suspended | API and MCP access is suspended for the organization (a platform kill switch, checked before every other gate) |
billing_suspended | The organization's account is suspended |
not_allowlisted | API access is disabled for the organization |
key_management_disabled | /api/v1/keys* is disabled for the organization |
mcp_disabled | /api/v1/mcp is disabled for the organization |
Organization Usage Caps
After the per-key limits, every request is counted on an organization ledger for the current UTC day and then for the current month, on every billing status. The organization's API profile carries the caps: limits.hardDailyRequests and limits.hardDailyEstimatedCostUsd for the day, limits.hardMonthlyRequests and limits.hardMonthlyEstimatedCostUsd for the month. For an organization with active billing (or an active manual override) a cap of 0 means no explicit cap (the request is still counted); for an organization without active billing a cap of 0 admits nothing. Request-count caps are always enforced; cost caps are recorded without refusing unless the deployment enables hard cost enforcement.
At a cap, an organization with active billing receives 429 RATE_LIMITED with retryAfter (seconds to the next UTC day or month, also in the Retry-After header); an organization without active billing receives 402 PAYMENT_REQUIRED with a billing URL. Both carry a machine-readable reason: hard_daily_request_cap_exceeded, hard_daily_cost_cap_exceeded, hard_monthly_request_cap_exceeded, or hard_monthly_cost_cap_exceeded. Raising a limit is a support request; there is no bypass.
Separately from request caps, each new screening run started through the API (POST /api/v1/packets/{submissionId}/review and the MCP start_packet_review tool) is reserved against the reviewing organization's review-spending budget (reviewBudget on the same profile: daily runs, daily and monthly estimated cost). These limits are enforced before new work is queued: a refusal returns 429 RATE_LIMITED with retryAfter. Reusing an already-active run does not reserve a second run. The recorded charges are estimates, not a ceiling on actual provider bills. See Errors & Retries for the response shapes.
Recommended retry strategy:
async function callWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status === 429) {
const body = await res.json();
const waitSeconds = body.error?.retryAfter ?? 60;
await new Promise(r => setTimeout(r, waitSeconds * 1000));
continue;
}
return res;
}
throw new Error('Rate limit exceeded after retries');
}Request Correlation
When X-Request-Id is present, include it when contacting support — it maps directly to audit log entries for rapid investigation. Most API-key-authenticated handler responses include it after authentication, organization-profile, rate-limit, and monthly-usage gates pass. An early failure at one of those gates can omit the header.
Key Security Best Practices
- Never expose keys in client-side code. API keys should only be used from server-side applications.
- Use environment separation. Use
gw_test_keys for development andgw_live_keys for production. - Restrict allowed endpoints. Scope keys to only the endpoints your integration needs (e.g.,
['/api/v1/jobs*', '/api/v1/documents/*']). - Set cost limits. Use
monthlyCostLimitUsdto prevent unexpected charges from runaway integrations. - Rotate keys periodically. Create a new key, update your integration, then revoke the old key.
- Set expiration dates. For time-limited integrations (POCs, contractor access), set
expiresAt.
Self-Service Key Management
Org owners and admins can manage API keys programmatically via the Keys API, the CLI, or the customer dashboard — no admin intervention required.
import { GridWonkClient } from '@gridwonk/sdk';
const client = new GridWonkClient({
apiKey: process.env.GRIDBOOST_API_KEY!,
});
// Create a new key for your organization
const { key } = await client.createKey({ name: 'Production Key' });
console.log(key.plaintext); // Store securely — shown only onceSee the full Key Management API reference for all endpoints, CLI commands, and security considerations.