# 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_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&#x20;*****to your*****&#x20;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

```pseudo
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 [**developers@bead.xyz**](mailto:developers@bead.xyz):

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