we-promise/sure · error · Provider::Mercury::MercuryError
access_forbidden
access_forbidden
Error message
Access forbidden - check your API token permissions
What it means
Raised by Provider::Mercury#handle_response when Mercury returns HTTP 403. Unlike 401, the token itself authenticated successfully, but it does not have permission for the requested resource — Mercury API tokens are created against a specific organization and can be scoped, so a valid token still gets 403 for resources outside its scope.
Source
Thrown at app/models/provider/mercury.rb:122
"Authorization" => "Bearer #{token}",
"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 "Mercury API: Bad request - #{response.body}"
raise MercuryError.new("Bad request to Mercury API: #{response.body}", :bad_request)
when 401
# Parse the error response for more specific messages
error_message = parse_error_message(response.body)
raise MercuryError.new(error_message, :unauthorized)
when 403
raise MercuryError.new("Access forbidden - check your API token permissions", :access_forbidden)
when 404
raise MercuryError.new("Resource not found", :not_found)
when 429
raise MercuryError.new("Rate limit exceeded. Please try again later.", :rate_limited)
else
Rails.logger.error "Mercury API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise MercuryError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
end
end
def parse_error_message(body)
parsed = JSON.parse(body, symbolize_names: true)
errors = parsed[:errors] || {}
case errors[:errorCode]
when "ipNotWhitelisted"
ip = errors[:ip] || "unknown"
"IP address not whitelisted (#{ip}). Add your IP to the API token's whitelist in Mercury dashboard."View on GitHub (pinned to e69894adb9)
Solutions
- Confirm the account_id you pass (get_account/get_account_transactions) is returned by get_accounts for the same token — if it is not, the token belongs to a different organization.
- In the Mercury dashboard, check which organization the API token was created under and recreate it under the organization that owns the account.
- Verify base_url is the default https://api.mercury.com/api/v1 unless you intentionally target another environment, and that the token matches that environment.
- If the token has a configurable scope, widen it to cover accounts and transactions.
Example fix
# before
accounts = provider.get_accounts
first = accounts[:accounts].first
provider.get_account(other_org_account_id) # 403
# after
accounts = provider.get_accounts
owned_ids = accounts[:accounts].map { |a| a[:id] }
raise ArgumentError, "account #{id} not visible to this token" unless owned_ids.include?(id)
provider.get_account(id) Defensive patterns
Strategy: try-catch
Validate before calling
# only query account IDs the token can actually see
visible = provider.get_accounts[:accounts].map { |a| a[:id] }
raise ArgumentError, "#{account_id} not owned by this token" unless visible.include?(account_id) Try / catch
begin provider.get_account(account_id) rescue Provider::Mercury::MercuryError => e case e.error_type when :access_forbidden then flag_wrong_org(item) # token vs account mismatch else raise end end
Prevention
- Treat the get_accounts list as the source of truth and never feed IDs from elsewhere.
- Pair the token and base_url environment (prod vs sandbox) in one config object so they cannot drift.
When it happens
Trigger: get_account(account_id) or get_account_transactions(account_id, ...) where account_id belongs to a different Mercury organization than the token's; a token whose scope excludes the account being queried; accessing an endpoint the token type does not allow.
Common situations: Sandbox/dev token used against production accounts or vice versa; user has multiple Mercury orgs and connected a token from the wrong one; account ID copied from the wrong workspace; base_url overridden to another environment while keeping the original token.
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/abdad91411b6e003.
Report an issue: GitHub.