we-promise/sure · error · LunchflowError
access_forbidden
access_forbidden
Error message
Access forbidden - check your API key permissions
What it means
HTTP 403 branch of Provider::Lunchflow#handle_response (app/models/provider/lunchflow.rb:130-131). Thrown as LunchflowError(:access_forbidden) with message 'Access forbidden - check your API key permissions' when Lunchflow returns 403: the x-api-key authenticated successfully but is not permitted to access the specific resource. Distinct from 401 — the key is valid, its authorization scope is not.
Source
Thrown at app/models/provider/lunchflow.rb:131
def auth_headers
{
"x-api-key" => api_key,
"Content-Type" => "application/json",
"Accept" => "application/json"
}
end
def handle_response(response)
case response.code
when 200
JSON.parse(response.body, symbolize_names: true)
when 400
Rails.logger.error "Lunch Flow API: Bad request - #{response.body}"
raise LunchflowError.new("Bad request to Lunch Flow API: #{response.body}", :bad_request)
when 401
raise LunchflowError.new("Invalid API key", :unauthorized)
when 403
raise LunchflowError.new("Access forbidden - check your API key permissions", :access_forbidden)
when 404
raise LunchflowError.new("Resource not found", :not_found)
when 429
raise LunchflowError.new("Rate limit exceeded. Please try again later.", :rate_limited)
else
Rails.logger.error "Lunch Flow API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise LunchflowError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
end
end
class LunchflowError < StandardError
attr_reader :error_type
def initialize(message, error_type = :unknown)
super(message)
@error_type = error_type
end
endView on GitHub (pinned to e69894adb9)
Solutions
- In the Lunchflow dashboard, check the key's permissions/scopes and grant access to the endpoints being called (accounts, transactions, balance, holdings).
- Confirm the account_id belongs to a workspace the key can reach.
- If the provider gates the endpoint by plan, upgrade or stop calling that endpoint for this connection.
- Handle :access_forbidden by disabling the affected sync feature for this provider rather than retrying — 403 is not transient.
Example fix
// before holdings = client.get_account_holdings(acct_id) rescue nil // after begin holdings = client.get_account_holdings(acct_id) rescue Provider::Lunchflow::LunchflowError => e raise unless e.error_type == :access_forbidden disable_holdings_sync_for!(provider_connection) # permanent config issue end
Defensive patterns
Strategy: try-catch
Type guard
def access_forbidden?(e) e.is_a?(Provider::Lunchflow::LunchflowError) && e.error_type == :access_forbidden end
Try / catch
begin client.get_account_holdings(acct_id) rescue Provider::Lunchflow::LunchflowError => e raise unless e.error_type == :access_forbidden connection.update!(holdings_enabled: false) inform_user_to_widen_key_scopes(connection) end
Prevention
- Provision keys with the exact scopes your sync uses (accounts, transactions, balance, holdings).
- Check key permissions during onboarding, not during nightly syncs.
- Treat 403 as permanent — disable the feature rather than retrying.
When it happens
Trigger: A read-only or transactions-only key calling get_account_holdings or get_account_balance on accounts outside its granted scope; per-account ACLs in Lunchflow excluding the key; workspace/plan-level restrictions on an endpoint (e.g. holdings reserved for paid tiers); server-side IP allowlist rejecting the app host.
Common situations: Keys provisioned for a narrower scope than the sync job assumes; provider changing entitlements (holding endpoints gated by plan); account shared between workspaces where the key only covers one.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/7f7896da4ed0086f.
Report an issue: GitHub.