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

Likely cause
How to confirm
How to resolve

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_idbead-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

  1. Dump request & response headers – confirm the Bearer token is present, well-formed, and not stale.

  2. Decode the JWT – validate aud, client_id, exp, and scope.

  3. Call a simple endpoint – e.g., GET /payments/tracking/{trackingId}. If that also returns 403, the issue is global (token or firewall).

  4. Check token vs resource IDs – the token ties you to one terminal; accessing another terminal’s resources returns 403.

  5. 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