Why is my request returning 403 Forbidden?
A 403 means Bead understood your request format but refused to fulfil it because your credentials lack permission for the target resource or action.
1 — Most common causes & fixes
Missing / malformed Authorization
header
Header absent or shows Bearer null
in request logs.
Add Authorization: Bearer {access_token}
exactly; no extra spaces or quotes.
Expired access token
Decode the JWT (jwt.io) — exp
is in the past.
Re-authenticate (POST /protocol/openid-connect/token
) and retry with the new token.
Wrong client_id
or scope
Token’s client_id
≠ bead-terminal
, or scope
lacks openid
.
Use the terminal credentials (bead-terminal
) from Bead Support; include scope=openid profile email
.
Terminal / merchant mismatch
Token’s terminalId
header vs body value differ (e.g., writing to another terminal).
Ensure terminalId
and merchantId
in the request belong to the authenticated terminal.
Webhook signature check failing (for calls to your server)
Your handler returns 403 to Bead; Bead retries.
Verify the x-webhook-signature
using the correct signingSecret
.
IP or WAF block
API gateway log shows request reached Bead but response is 403 from your reverse-proxy.
Allow Bead IP ranges or relax geo/IP filtering.
CORS pre-flight denied (browser apps)
Browser console shows CORS policy: Response to preflight… 403
.
Proxy calls through your backend or add Bead origin to your allowed CORS list.
2 — Debug checklist
Dump request & response headers – confirm the Bearer token is present, well-formed, and not stale.
Decode the JWT – validate
aud
,client_id
,exp
, andscope
.Call a simple endpoint – e.g.,
GET /payments/tracking/{trackingId}
. If that also returns 403, the issue is global (token or firewall).Check token vs resource IDs – the token ties you to one terminal; accessing another terminal’s resources returns 403.
Inspect network middle-boxes – WAFs often rewrite or drop auth headers; pause them temporarily.
3 — Automated recovery pattern
if response.status == 403:
refresh_token()
retry_once()
if still 403:
escalate_to_log()
Refreshing the token catches >90 % of accidental 403s due to expiry.
4 — Still blocked?
Send the following to [email protected]:
Timestamp & timezone
Full request path (omit secrets)
x-request-id
header (if present)The first 50 chars of the JWT (
eyJ0eXAiOiJK…
) for token lookup
We’ll trace the request in our logs and identify the exact policy that triggered the 403.
Last updated