A bug was filed saying Orbit misreports its own bug as your permissions problem. It was a permissions problem — those were two different API keys
On 8 September a raw POST /canvas/duplicate returned 202 while Orbit's own orbit_create_braze_canvas returned 403 on the same endpoint. That went into the braze-claude-in-chrome-build skill as a workaround — try the raw endpoint before concluding the key lacks the scope — which is the documentation telling you Orbit's request is malformed. A bug was filed on exactly that premise. The premise was wrong, and wrong in the most inconvenient direction: those were two different API keys. Probed on 16 September with a deliberately invalid canvas_id, which creates nothing and separates the permission layer from the validation layer cleanly, the key holding canvas.duplicate came back 400 — 'canvas_id' must be a string of the object api identifier — and the key without it came back 403 Access Denied. Braze refuses on permission before it validates the body. So the 403 was never the payload and never the source canvas id; the same 403 comes back for any id, valid or not. Orbit's request shape was correct the whole time, and the skill spent eight days sending people around a bug that did not exist. The defect was the error, not the request, and that is what this release fixes.
What shipped
•Bug fix — a 403 or 401 from POST /canvas/duplicate returns status: auth_failed with braze_status, instead of a generic braze_api_error carrying Braze's bare Access Denied. The message names the fix rather than the symptom: canvas.duplicate is granted separately from the canvas.* read permissions everything else in Orbit uses, so a key that reads canvases perfectly well still fails on this one call. Settings → API Keys → the key in ORBIT_BRAZE_API_KEY → tick canvas.duplicate. It also states outright that the request and the canvas id are not the problem, because that is precisely where the first diagnosis went and the error was what sent it there.
•Behaviour change — the raw Braze text is kept in a braze_message field rather than replaced. Explaining an error is not the same as overwriting it, and a genuine permission refusal should still be readable in the provider's own words underneath Orbit's reading of it.
•Fix — brazePost attaches the HTTP status to the error it throws, as err.status. Callers that needed to tell a permission refusal from a bad request were pulling the number back out of the message string with a regular expression, which works until someone rewords the message and then fails silently by classifying every refusal as something else.
•Correction — braze-claude-in-chrome-build no longer carries the workaround. The note that said to try the raw endpoint first is replaced by the two-row probe table, the rule it proves — Braze rejects on permission before it validates the body — and the one checkbox that fixes it. The old note was written from a single observation that looked decisive and had an uncontrolled variable in it.
•Guard — six tests in tests/suites/78-canvas-duplicate-403.test.mjs. They pin the accepted request shape (method, URL, bearer header, canvas_id, name, description and tag_names, and no app_group_id), 403 and 401 both classifying as auth_failed, the message naming the permission and where to tick it, the raw Braze text surviving, and a 400 staying a plain API error — only auth is reclassified, because a reclassification that swallowed validation errors would hide the next real malformed request. Full suite: 1474 passed, 0 failed.