Docs/Error Codes

Error Codes

All errors returned by PermitNetworks follow a consistent structure. Machine-readable error codes let you handle specific failure cases in your application.

Error response shape (HTTP 4xx/5xx)
{
  "error": {
    "code": "auth.invalid_key",      // machine-readable code
    "message": "API key is invalid", // human-readable description
    "request_id": "req_01hwxyz..."   // include this when contacting support
  }
}

// Note: decision deny is NOT an HTTP error — it returns 200:
{
  "id": "dec_01hwxyz...",
  "effect": "deny",
  "reason": "policy.not_found",     // same code format
  ...
}

auth

4 codes
auth.invalid_keyHTTP 401API key is invalid

The API key passed in the Authorization header does not exist, has been revoked, or has a typo.

Fix:

Verify the key in Dashboard → API Keys. Ensure you are using the full key including the pn_live_sk_ or pn_test_sk_ prefix.

auth.expiredHTTP 401Permit token has expired

The Ed25519 permit token passed to a downstream service has exceeded its TTL (default: 5 minutes).

Fix:

Request a new authorization via permit.authorize(). Consider reducing the time between authorization and action execution.

auth.signature_invalidHTTP 401Permit token signature failed verification

The Ed25519 signature on the permit token is not valid. This can happen if the token was tampered with, or if you are verifying with the wrong public key.

Fix:

Fetch the current public key from /.well-known/jwks.json and re-verify. Never store public keys for long periods — refresh them periodically.

auth.scope_insufficientHTTP 403API key lacks required scope

The operation requires a scope that the current API key does not have. For example, creating a policy requires policies:write scope.

Fix:

Create a new API key with the required scope, or add the scope to the existing key in Dashboard → API Keys.

policy

3 codes
policy.not_foundHTTP 200No matching policy (default deny)

The authorization request did not match any enabled policy. PermitNetworks defaults to deny when no policy matches.

Fix:

Create a policy in the Dashboard or via POST /v1/policies that covers this agent + action combination.

policy.conflictHTTP 200Multiple policies matched with conflicting effects

Two or more policies matched the request with different effects. The lowest-priority-number policy wins. This is returned as a warning in the decision metadata.

Fix:

Review overlapping policies and clarify intent by adjusting priorities, adding agent-specific filters, or narrowing action/resource patterns.

policy.disabledHTTP 200Matching policy is disabled

A policy matched but is in disabled state. It is treated as if it does not exist for authorization purposes.

Fix:

Enable the policy in the Dashboard or via PUT /v1/policies/:id.

rate limit

1 code
rate_limit.exceededHTTP 429Request rate limit exceeded

The agent has exceeded the rate limit defined in the matching policy (per_second, per_minute, etc.), or the API key has exceeded the platform-level rate limit.

Fix:

Implement exponential backoff using the Retry-After response header. For persistent issues, review policy rate_limit settings or contact support to increase platform limits.

budget

2 codes
budget.exceededHTTP 200Agent budget limit reached

The agent's budget (fiat or API call) has been fully consumed for the current period. All subsequent requests from this agent will be denied until the period resets.

Fix:

Wait for the budget period to reset, or increase the budget limit in the policy. A budget.exceeded webhook is fired when this happens.

budget.below_thresholdHTTP 200Budget threshold crossed (informational)

This is not an error — it appears in webhook events when budget consumption crosses the configured threshold_alert percentage. Decisions still return allow.

Fix:

No action required. Configure a budget.threshold webhook handler to alert your team before the limit is reached.

resource

1 code
resource.not_allowedHTTP 200Resource is outside the policy scope

The requested resource did not match the resource patterns defined in any allow rule. The policy explicitly restricts this resource.

Fix:

Check that the resource string matches the patterns in your policy. Ensure you are not passing a more specific resource than the policy's wildcard covers.

decision

3 codes
decision.already_confirmedHTTP 409Decision was already confirmed

You called confirm() on a decision that is already in confirmed state. This is a no-op but returns 409 to prevent double-counting.

Fix:

Track decision state in your application. Do not call confirm() more than once per decision ID.

decision.expiredHTTP 410Decision has expired

The decision's TTL has elapsed. It can no longer be confirmed or rejected. The permit token derived from this decision is also expired.

Fix:

Request a new authorization via permit.authorize(). Reduce latency between authorization and action execution.

decision.not_foundHTTP 404Decision ID does not exist

The decision ID was not found. Either it was never created, belongs to a different account, or was from a different environment.

Fix:

Verify the decision ID. Check you are using the correct API key (test vs live environment).

validation

2 codes
validation.missing_fieldHTTP 400Required field missing in request body

A required field (e.g. agent_id or action in POST /v1/decisions) was not included in the request body.

Fix:

Check the API reference for required fields and ensure your request body is valid JSON.

validation.invalid_actionHTTP 400Action string is not valid

The action string contains invalid characters or does not follow the namespace.verb convention.

Fix:

Actions must match the pattern [a-z][a-z0-9_]*\.[a-z][a-z0-9_]* (e.g. payment.create, data.read). Wildcards (*) are only valid in policy definitions, not in authorization requests.

Need Help?

If you are seeing an error not listed here, or an error you cannot resolve, contact support with the request_id from the error response. Each request has a unique ID we can use to look up the full trace.