Appearance
For the operator's engineer debugging a Connect setup or a failed conformance run.
Troubleshooting
Start with connect_status (or GET /operators/me/connect/status): each module carries a blocking line that says what to do next. For a failed run, connect_conformance_evidence (or GET /operators/me/connect/conformance/<runId>) shows every request OneHazel sent and every response you returned, with secrets masked.
Platform error codes below are listed with their meaning on the error reference.
Access and keys
| Symptom | Cause | Fix |
|---|---|---|
Every /operators/me/connect/* REST call answers HTTP 403 CONNECT_DISABLED, and every connect_* tool answers JSON-RPC error -32001 with CONNECT_DISABLED | Connect is switched off for this environment. It is one switch for both surfaces. | Ask for access through the chat button in the OneHazel app. After it is switched on, allow up to 30 seconds before retrying. |
A connect_* tool answers -32001 with MCP_CONNECT_NOT_ENABLED, and tools/list shows no connect_* tools | The API key does not carry the Connect scope. Keys created on the API Keys page never have it. | This key has no Connect scope. Create one in app.onehazel.com under Connect casino, Integrate with AI (the API Keys page cannot add it). |
A REST call to /operators/me/connect/* answers 401 AUTH_MISSING with an oh_live_ key | These routes accept only a signed-in session token, not an API key. | Call them with your session's bearer token, or use the MCP tools with the API key. |
| The stdio bridge connects but every call fails, or the bridge exits with code 2 | Exit 2 means ONEHAZEL_API_KEY is not set. Otherwise the bridge may be forwarding to an address that does not answer. | Set ONEHAZEL_API_KEY. To pin the server address, set ONEHAZEL_MCP_URL=https://api.onehazel.com/mcp-server. You need that override only on @onehazel/mcp 0.1.0 installs, because 0.1.1 and later default to /mcp-server. |
Registering an endpoint
| Symptom | Cause | Fix |
|---|---|---|
CONNECT_INVALID_URL with "baseUrl must use https" | The base URL is http://. OneHazel sends the callback key and signed bodies to it, so plain http is refused. | Serve the module over https and register the https:// URL. |
CONNECT_INVALID_URL with "URL points at a private or internal address", "baseUrl does not resolve to a public address" or "URLs with embedded credentials are not allowed" | The SSRF guard refused the URL: a private or loopback IP, a localhost-style or internal name, a host that resolves to a private address or does not resolve at all, or user:pass@ in the URL. | Register a public hostname that resolves to a public IP, with no credentials in the URL. For local testing, put the sample behind a public https tunnel. |
VALIDATION_ERROR "wallet registration requires testPlayerId" | The wallet module must name its test player. | Register again with testPlayerId set to a funded player in your test ledger. |
VALIDATION_ERROR "player registration requires testPlayerId" or "player registration requires restrictedPlayerId" | The player module must name both test players. Registering again keeps stored values, so this means the field was never set or was cleared with null. | Register again with the missing field: testPlayerId (a player in your test ledger) or restrictedPlayerId (a test player with an active restriction). |
VALIDATION_ERROR "wallet registration requires testGame" | The wallet module must name its test game. | Register again with testGame: { provider_slug, game_code }, one active game in your catalogue that test sessions may use. |
Conformance run errored with reason wallet_route_not_found | session/start answered 404 with no contract error body, so nothing at your registered URL speaks the contract. | Check the registered base URL (it must be the full prefix the contract paths follow), fix it with connect_register_endpoint, then run conformance again. |
Conformance run errored with reason test_game_rejected | Your PAM refused the declared test game at session/start. The detail reads "The game you declared was rejected by your casino, check provider_slug and game_code." plus your status and code. | Register the wallet again with a testGame your PAM accepts under X-OneHazel-Env: test, then run conformance again. |
CONNECT_MODULE_NOT_REGISTRABLE | Only wallet, player and webhook.receive take a registered endpoint. | The other modules are derived; see their module pages. |
Running conformance
| Symptom | Cause | Fix |
|---|---|---|
CONNECT_MODULE_NOT_REGISTERED when starting a run | No endpoint is registered for that module. | Register it first. |
RATE_LIMITED when starting a run | At most 10 conformance runs per operator per hour. | Wait, and use the samples and signForTest to check locally between runs. |
A player run ends errored with reason missing_input, naming restrictedPlayerId or testPlayerId | The player module needs both a test player and a restricted test player, and one is not stored. connect_status lists it under requiredInputs. | Register the player endpoint again with both testPlayerId and restrictedPlayerId (at most 128 characters), then run again. |
lookup.restricted_flag fails with an empty restrictions array | The player named as restrictedPlayerId has no active restriction in your test ledger, or the lookup ignores it. | Give that test player an active restriction (a limit, a block or a self-exclusion) and return it in restrictions. |
A run ends errored with reason ssrf_blocked | The base URL was re-checked when the run started, or on a later call, and now points at a private address or no longer resolves. | Make the host resolve only to public addresses. |
A scenario fails with a 301, 302 or other redirect as your response | OneHazel never follows redirects, so the redirect itself is recorded as your answer. | Serve the contract paths directly at the registered base URL. |
Signature scenarios
| Symptom | Cause | Fix |
|---|---|---|
auth.stale_timestamp fails, or real calls are rejected as stale | Your check accepts timestamps outside 300 s, or your server clock has drifted. The scenario sends a correct MAC with a timestamp 600 s old. | Reject any t more than 300 s from your clock, and keep the clock synced with NTP. |
auth.bad_signature or deliver.tampered_rejected fails | Your server accepts a body whose MAC does not match. | Compute the HMAC over the raw bytes and reject on mismatch, comparing in constant time. |
| Every signed call is rejected by your own server | The MAC is computed over a re-serialised body. Parsing JSON and stringifying it again changes spacing and key order, so the MAC never matches. | Capture the raw request bytes before any body parser runs and verify those. Each sample shows how for its framework. |
| Every signed call is rejected after a rotation | The module still holds the old secret, or holds only the new one while a run queued earlier signs with the old one. | Set the new secret, and keep the old one as the previous secret for an hour (see below). |
| Wallet calls rejected, webhook deliveries fine, or the other way round | The two formats differ. Wallet and player use t=<ts>,v1=<hex> over "<t>." + raw body plus a bearer; webhook.receive uses sha256=<hex> over the raw body with no bearer and no timestamp. | Use the verifier for the right module: verifySignature for wallet and player, verifyWebhookSignature for webhooks. |
Environment header
| Symptom | Cause | Fix |
|---|---|---|
env.test_header_required fails | One of: your server accepts a call with no X-OneHazel-Env, or a test call resolves to a player other than the testPlayerId you registered. | Reject calls without the header, and route test traffic to the test ledger where the registered test player lives. |
A webhook delivery is rejected for its X-OneHazel-Env header | Live deliveries carry no X-OneHazel-Env; conformance deliveries carry X-OneHazel-Env: test. | Do not require or reject the header on webhook.receive. |
Status and secrets
| Symptom | Cause | Fix |
|---|---|---|
The chip or connect_status still shows the old state after a new run started | Not a defect. Only a finished run carries a verdict, so a queued or running run never replaces the previous result. The app's status card also refreshes every 30 seconds while the tab is open. | Wait for the run to finish. connect_status reads fresh on every call; the app updates on its next refresh or when you return to the tab. |
A module shows conformant but evidence: derived | No conformance run backs that module; its state comes from a rule. In v1 that is launch, widget, attribution and events.push. | Nothing to fix. See the module pages for each rule. |
Calls signed with the old secret start failing an hour after connect_rotate_secret | A rotation keeps the previous secret valid for one hour, then only the new one verifies. | Deploy the new secret within the hour. During the overlap set it as the current secret and the old one as ONEHAZEL_SIGNING_SECRET_PREVIOUS in the samples, then drop the old one. |
| Registering an endpoint again returned no signing secret and no callback key | Registering a module that already exists keeps both: the response says secretUnchanged: true and changes only the base URLs, test players and test game. | Nothing to deploy and no restart: keep the values you already stored. To replace them, use the rotate route or connect_rotate_secret. |