Support request — GET /v1/workspace returns 403 with an empty body
Subject: GET /v1/workspace returns HTTP 403 with an empty body — which API user role is required, and is the workspaceId in a webhook envelope the same identifier?
Workspace / key details
| Workspace name | |
| Workspace type | Developer Sandbox |
| API base URL | https://sandbox-api.fireblocks.io |
| API key ID (first 8 chars) | |
| Role assigned to that API user | Editor, no sign admin, security advisor |
workspaceId seen in our webhook envelopes redacted |
|
| Approximate time of a failed call (UTC) | <2026-08-14 09:15Z> |
What we are building, and why we need the workspace ID
We are integrating Fireblocks for Litecoin deposits and payouts in our payment gateway, and we consume Webhooks v2 (detached JWS, verified against the JWKS for our environment).
Signature verification proves that an event was signed by Fireblocks, but the JWKS is published per environment, not per workspace — so a valid signature does not prove the event came from our workspace rather than from another tenant of the same environment. To close that gap we pin every one of our merchant contracts to the workspaceId of the workspace it belongs to, and refuse any inbound event whose workspaceId does not match the pinned value.
That means the workspace ID must be obtained out of band, as a configuration value. Reading it from an inbound webhook would defeat the control entirely: an attacker sending a correctly signed event from their own workspace would simply teach us their ID.
Where the documentation says to find it
GET /v1/workspace — “Returns the workspace ID and name for the authenticated user” Get workspace - Fireblocks Developer Docs
As far as we can tell this is the only documented way to obtain the value. It is not displayed anywhere we could find in the console.
We have since observed that the Webhooks v2 envelope carries a workspaceId field, and we have read a value from it by making a test deposit into our own workspace. That gives us a candidate value, but we do not consider it a substitute for the API:
- It is inbound data. Sourcing the configuration of a control that exists to validate inbound data from that same inbound data defeats the control — an attacker delivering a correctly signed event from their own workspace during onboarding would simply teach us their ID.
- It requires a transaction to have happened. We need to configure this for staging and production workspaces where making a test deposit first is not a reasonable prerequisite.
So the API remains what we want to rely on, and the observed value is something we would like confirmed rather than trusted.
What does not work
Calling GET https://sandbox-api.fireblocks.io/v1/workspace returns:
HTTP 403
(empty response body)
There is no JSON error object and no Fireblocks error code, which makes the refusal hard to distinguish from other failure modes.
Points that we believe rule out the obvious causes:
- The request is correctly signed. We use the documented scheme —
X-API-Keyheader plus an RS256 JWT with theuri,nonce,iat,exp,subandbodyHashclaims, withexpless thaniat + 30s. An earlier attempt with a longer expiry window did return{"message": "The request has expired", "code": -10}; after shortening it, that error is gone and we get the 403 instead. - The path is right. An earlier attempt against
/v1/management/workspacereturned{"message": "Endpoint not defined in API specification", "code": -15}./v1/workspacedoes not return-15, so the endpoint exists. - The same key and the same signing code work elsewhere. With no change other than the URI, the identical procedure succeeds against
/v1/supported_assetsand/v1/vault/accounts_paged. Only/v1/workspaceis refused.
Minimal reproduction
API_KEY="<api key id>"
SECRET_KEY="<path to the private key .pem>"
BASE_URL="https://sandbox-api.fireblocks.io"
URI="/v1/workspace"
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
now=$(date +%s)
body_hash=$(printf '' | openssl dgst -sha256 | awk '{print $NF}')
header=$(printf '{"alg":"RS256","typ":"JWT"}' | b64url)
payload=$(printf '{"uri":"%s","nonce":"%s","iat":%d,"exp":%d,"sub":"%s","bodyHash":"%s"}' \
"$URI" "$(uuidgen)" "$now" "$((now + 10))" "$API_KEY" "$body_hash" | b64url)
signature=$(printf '%s.%s' "$header" "$payload" | openssl dgst -sha256 -sign "$SECRET_KEY" | b64url)
curl -sS -w '\nHTTP %{http_code}\n' "$BASE_URL$URI" \
-H "X-API-Key: $API_KEY" \
-H "Authorization: Bearer $header.$payload.$signature"
Swapping URI for /v1/supported_assets in the same script returns HTTP 200 and a valid payload.
Questions
-
Is the
workspaceIdin a Webhooks v2 envelope the same identifier thatGET /v1/workspacereturns asid? This is our most important question. Our check compares the envelope’sworkspaceIdagainst a configured value that we intend to read from/v1/workspace. If the two are different identifiers, or drawn from different namespaces, the comparison would never match and we would silently refuse every genuine deposit and payout notification. Could you also confirm that<uuid observed in the webhook>is the correct workspace ID for the workspace named above? -
Which API user role is required for
GET /v1/workspace? The API reference page for this endpoint carries no Endpoint Permission line, whereas other endpoints do — for exampleGET /v1/management/audit_logsstates “Admin, Non-Signing Admin, Auditor, Security Admin, Security Auditor”, and the workspace status (Beta) endpoint states “This endpoint is available only for API keys with Admin/Non Signing Admin permissions.” If/v1/workspacehas a role requirement, could it please be documented on that page? -
Is a read-only role sufficient? We would strongly prefer not to issue an Admin or Non-Signing Admin API key for the sole purpose of reading one UUID. Can an API user with the Security Auditor (or Auditor) role call this endpoint? The audit logs endpoint accepts those roles, so we assume the auditor roles are intended for exactly this kind of read.
-
Is this endpoint served on Developer Sandbox workspaces at all? If it is not, we will read the value from a Testnet workspace instead — but we would like to know rather than guess.
-
If the endpoint is unavailable to us, what is the supported way to obtain the ID of a workspace we own? Is the value exposed anywhere in the console UI? We are specifically looking for a method that does not involve trusting an inbound webhook.
-
Should a 403 from this endpoint carry a response body? Every other refusal we have seen from the API returns a JSON object with a
messageand acode. The empty body here gave us no signal to act on.
Thank you!