{"openapi":"3.1.0","info":{"title":"Fibgle — Google Business Profile API twin","version":"1.0.0","description":"Fibgle is an internal, deliberately byte-faithful **digital twin** of the Google Business\nProfile API (OAuth + Account Management v1 + Business Information v1 + legacy Google My\nBusiness v4 reviews). rosie's real Google integration can point at Fibgle by swapping base\nURLs; Fibgle answers on the same host/path shapes with byte-identical envelopes.\n\n**Only the `oauth`, `accounts-v1`, `locations-v1`, and `reviews-v4` tags are byte-faithful\nto real Google.** The `meta`, `public`, and `operator` tags are Fibgle-only test\naffordances (a how-to home page, a Google-styled place page, and a seed/admin console) and\nare NOT part of the Google contract.\n\nKnown intentional divergences and the exact wire rules live in the as-built docs:\n`apps/fibgle/docs/deviations.md` and `apps/fibgle/docs/fidelity-contract.md`. The standing\ncaveat (the base-URL-swap premise is undemonstrated pending rosie's real GBP client) is in\n`deviations.md`.\n\nNote: an off-by-default test knob (`FIBGLE_FORCE_429`) makes every `/v1/*` and `/v4/*`\ndata-API request return `429 RESOURCE_EXHAUSTED` (with `Retry-After`); it is not listed\nper-operation below since it never fires in normal use."},"servers":[{"url":"https://fibgle.fiberplane.io","description":"prod stage (bare canonical host)"},{"url":"https://fibgle-{stage}.fiberplane.io","description":"any non-prod stage","variables":{"stage":{"default":"spike"}}},{"url":"http://localhost:8787","description":"local `bun run dev` (alchemy dev)"}],"tags":[{"name":"meta","description":"Public probes + docs. No auth, no gating. Fibgle-only (not a Google surface)."},{"name":"oauth","description":"accounts.google.com / oauth2.googleapis.com shapes. FLAT {error,error_description} envelope. Byte-faithful to Google."},{"name":"accounts-v1","description":"Account Management v1 (mybusinessaccountmanagement.googleapis.com/v1). google.rpc.Status envelope. Byte-faithful."},{"name":"locations-v1","description":"Business Information v1 (mybusinessbusinessinformation.googleapis.com/v1). readMask REQUIRED. Byte-faithful."},{"name":"reviews-v4","description":"Legacy GMB v4 (mybusiness.googleapis.com/v4) — the poll surface. google.rpc.Status + error.errors[]. Byte-faithful."},{"name":"public","description":"The /m 'looks like Google' SSR place page + write composer. Fibgle-only test affordance (HTML)."},{"name":"operator","description":"Hidden, NON-Google seed/admin/sim surfaces gated by FIBGLE_SEED_TOKEN (seedGate). Secure default: 404 when the token is UNSET; opaque 404 on a wrong/missing token. Fibgle-only."}],"security":[],"paths":{"/healthz":{"get":{"tags":["meta"],"operationId":"healthz","summary":"Liveness probe","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"service":{"type":"string","example":"fibgle"}}}}}}}}},"/build-info":{"get":{"tags":["meta"],"operationId":"buildInfo","summary":"Deploy provenance (commit + buildTime); Cache-Control: no-store","responses":{"200":{"description":"build provenance","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":["string","null"]}}}}}}}}},"/":{"get":{"tags":["meta"],"operationId":"home","summary":"In-app 'how to use Fibgle' home page (HTML, static copy — not the /m place page)","responses":{"200":{"description":"HTML landing/docs page","content":{"text/html":{}}}}}},"/openapi.json":{"get":{"tags":["meta"],"operationId":"openapiDoc","summary":"This OpenAPI 3.1 document","responses":{"200":{"description":"the OpenAPI document","content":{"application/json":{}}}}}},"/docs":{"get":{"tags":["meta"],"operationId":"docs","summary":"Rendered API reference (Redoc over /openapi.json)","responses":{"200":{"description":"HTML API reference","content":{"text/html":{}}}}}},"/o/oauth2/v2/auth":{"get":{"tags":["oauth"],"operationId":"authorize","summary":"Authorize / consent. Validates client_id + EXACT redirect_uri BEFORE redirecting.","description":"A bad/unknown client_id or an unregistered redirect_uri is an error PAGE (HTML 400), never a redirect to an untrusted URI. Once the redirect_uri is trusted, errors (unsupported_response_type, invalid_scope, access_denied) are redirected back with `?error=`. `FIBGLE_AUTO_APPROVE` (default on) or `?approve=1` skips the consent click; `?fibgle_user=` picks the mock identity stamped onto the code.","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"},"description":"Must EXACTLY match a registered redirect URI."},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","default":"https://www.googleapis.com/auth/business.manage"}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"code_challenge","in":"query","required":false,"schema":{"type":"string"},"description":"PKCE S256 challenge. If present, /token requires a matching code_verifier; if absent, /token rejects any code_verifier (downgrade guard)."},{"name":"code_challenge_method","in":"query","required":false,"schema":{"type":"string","enum":["S256"]}},{"name":"fibgle_user","in":"query","required":false,"schema":{"type":"string","default":"user-rosie-demo"},"description":"Fibgle-only: the mock consent identity (no real Google sign-in)."},{"name":"approve","in":"query","required":false,"schema":{"type":"string","enum":["1"]}},{"name":"deny","in":"query","required":false,"schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The consent page (only when auto-approve is off).","content":{"text/html":{}}},"302":{"description":"Redirect back to redirect_uri with ?code=&state= (success) or ?error=&state= (post-validation denial).","headers":{"Location":{"schema":{"type":"string"}}}},"400":{"description":"Authorization error PAGE (bad client_id / redirect_uri mismatch).","content":{"text/html":{}}}}}},"/token":{"post":{"tags":["oauth"],"operationId":"token","summary":"Token endpoint — authorization_code (PKCE + ~120s idempotent replay) and refresh_token grants","description":"The code grant returns an access + refresh pair; the refresh grant returns a fresh access token and OMITS refresh_token (no rotation). Re-exchanging the same code within ~120s replays the same pair verbatim. A MISSING client_id is 400 invalid_request; an unknown client or wrong secret is 401 invalid_client.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Token response (Cache-Control: no-store).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"invalid_request / invalid_grant / unsupported_grant_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client (unknown client or wrong secret)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/oauth2/v4/token":{"post":{"tags":["oauth"],"operationId":"tokenV4Alias","summary":"Alias of POST /token (same handler)","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Token response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"invalid_request / invalid_grant / unsupported_grant_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/revoke":{"post":{"tags":["oauth"],"operationId":"revoke","summary":"Revoke a token (access OR refresh) — cascades to the grant","description":"Resolves the token to its grant and sets grant.revoked=true. An unknown token returns 400 invalid_token (proposal-specified; diverges from RFC 7009's lenient 200 — see deviations.md #7).","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"Revoked (empty body)."},"400":{"description":"invalid_request (no token) / invalid_token (unknown token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/v1/userinfo":{"get":{"tags":["oauth"],"operationId":"userinfo","summary":"OpenID-style userinfo (FLAT OAuth 401 envelope; does NOT require business.manage)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"userinfo claims","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"401":{"description":"invalid_token (missing/unknown/revoked/expired)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/v1/accounts":{"get":{"tags":["accounts-v1"],"operationId":"listAccounts","summary":"accounts.list — the bearer user's accounts (keyset paging; pageSize default/max 20)","security":[{"bearerAuth":[]}],"parameters":[{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","default":20,"maximum":20},"description":"Clamped silently to [1,20]."},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor. nextPageToken is ABSENT on the last page."}],"responses":{"200":{"description":"accounts page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAccountsResponse"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"403":{"description":"PERMISSION_DENIED (token lacks business.manage)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}}}}},"/v1/accounts/{accountId}":{"get":{"tags":["accounts-v1"],"operationId":"getAccount","summary":"accounts.get (unknown/unowned → 404 NOT_FOUND)","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"403":{"description":"PERMISSION_DENIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"404":{"description":"NOT_FOUND (unknown or unowned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}}}}},"/v1/accounts/{accountId}/locations":{"get":{"tags":["locations-v1"],"operationId":"listLocations","summary":"locations.list — readMask REQUIRED; accepts accounts/{id} or accounts/-","description":"readMask is REQUIRED (absent/empty → 400 INVALID_ARGUMENT with fieldViolations[].field=read_mask). pageSize default 10 / max 100. totalSize is emitted ONLY when a filter is present.","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"},"description":"An account id, or `-` for all the caller's accounts."},{"name":"readMask","in":"query","required":true,"schema":{"type":"string","example":"name,title"},"description":"FieldMask. `*` returns the full resource; unknown fields are ignored; an array-index segment (x.0) is 400."},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","default":10,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string"},"description":"Presence-only in Fibgle: triggers totalSize but is not evaluated (deviations.md #5)."}],"responses":{"200":{"description":"locations page (projected to readMask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListLocationsResponse"}}}},"400":{"description":"INVALID_ARGUMENT (missing/invalid read_mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"403":{"description":"PERMISSION_DENIED (unowned account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}}}}},"/v1/locations/{locationId}":{"get":{"tags":["locations-v1"],"operationId":"getLocation","summary":"locations.get (bare name locations/{id}); readMask REQUIRED","security":[{"bearerAuth":[]}],"parameters":[{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"readMask","in":"query","required":true,"schema":{"type":"string","example":"name,title,metadata"}}],"responses":{"200":{"description":"the location (projected)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Location"}}}},"400":{"description":"INVALID_ARGUMENT (missing/invalid read_mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"403":{"description":"PERMISSION_DENIED (caller doesn't own the account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}},"404":{"description":"NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatus"}}}}}}},"/v4/accounts/{accountId}/locations/{locationId}/reviews":{"get":{"tags":["reviews-v4"],"operationId":"listReviews","summary":"reviews.list — keyset paging, aggregates on EVERY page, NO time filter","description":"averageRating (1-decimal, omitted if no rated reviews) + totalReviewCount ride on every page. nextPageToken is ABSENT on the last page. There is NO `since`/time-filter param — clients filter incrementally by updateTime. A pageToken is order-coupled: reusing it under a different orderBy → 400 INVALID_ARGUMENT.","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}},{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"orderBy","in":"query","required":false,"schema":{"type":"string","enum":["updateTime desc","rating","rating desc"],"default":"updateTime desc"}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":50}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"reviews page + aggregates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListReviewsResponse"}}}},"400":{"description":"INVALID_ARGUMENT (bad orderBy or pageToken/orderBy mismatch)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"403":{"description":"PERMISSION_DENIED (unowned account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"404":{"description":"NOT_FOUND (missing/mismatched location)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}}}}},"/v4/accounts/{accountId}/locations/{locationId}/reviews/{reviewId}":{"get":{"tags":["reviews-v4"],"operationId":"getReview","summary":"reviews.get","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}},{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"403":{"description":"PERMISSION_DENIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"404":{"description":"NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}}}}},"/v4/accounts/{accountId}/locations/{locationId}/reviews/{reviewId}/reply":{"put":{"tags":["reviews-v4"],"operationId":"updateReply","summary":"reviews.updateReply — UPSERT {comment}; bumps the parent review's updateTime","description":"Returns {comment, updateTime} with a fresh server updateTime (any client-sent updateTime is ignored). The bump re-sorts the review to the top of the default updateTime-desc order.","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}},{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string"}}}}}},"responses":{"200":{"description":"the reply","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewReply"}}}},"400":{"description":"INVALID_ARGUMENT (empty comment)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"403":{"description":"PERMISSION_DENIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"404":{"description":"NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}}}},"delete":{"tags":["reviews-v4"],"operationId":"deleteReply","summary":"reviews.deleteReply — {} on success; NOT_FOUND if the review has no reply","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}},{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"deleted (empty object)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"403":{"description":"PERMISSION_DENIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"404":{"description":"NOT_FOUND (no review, or no reply to delete)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}}}}},"/v4/accounts/{accountId}/locations:batchGetReviews":{"post":{"tags":["reviews-v4"],"operationId":"batchGetReviews","summary":"batchGetReviews — SHAPE-ONLY P2 stub ({locationReviews:[{name,review}]}, no aggregates)","description":"rosie does not call this; it exists only so a client that does gets the right envelope (deviations.md #6).","security":[{"bearerAuth":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"locationNames":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"the batch","content":{"application/json":{"schema":{"type":"object","properties":{"locationReviews":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"review":{"$ref":"#/components/schemas/Review"}}}}}}}}},"401":{"description":"UNAUTHENTICATED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}},"403":{"description":"PERMISSION_DENIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcStatusV4"}}}}}}},"/m/locations/{locationId}":{"get":{"tags":["public"],"operationId":"placePage","summary":"SSR 'looks like Google' place page (card + reviews + UI-only histogram)","parameters":[{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"place page","content":{"text/html":{}}},"404":{"description":"HTML 'place not found' page","content":{"text/html":{}}}}}},"/m/locations/{locationId}/write":{"get":{"tags":["public"],"operationId":"writeComposer","summary":"SSR 'Write a review' composer with the mock identity picker","parameters":[{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"rating","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":5,"default":5}}],"responses":{"200":{"description":"composer","content":{"text/html":{}}},"404":{"description":"HTML not-found","content":{"text/html":{}}}}}},"/m/locations/{locationId}/reviews":{"post":{"tags":["public"],"operationId":"createReviewFromComposer","summary":"Insert one simulated review, then 303 back to the place page","description":"A P0 simulation write: the row is exactly what reviews.list returns on the next poll.","parameters":[{"name":"locationId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"identity":{"type":"string","description":"persona:<id> | anon | new"},"newName":{"type":"string"},"comment":{"type":"string"}}}}}},"responses":{"303":{"description":"redirect to the place page","headers":{"Location":{"schema":{"type":"string"}}}},"404":{"description":"unknown location (bare JSON)","content":{"application/json":{}}}}}},"/admin":{"get":{"tags":["operator"],"operationId":"adminDashboard","summary":"SSR operator dashboard (per-location aggregates + scenario runner)","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"responses":{"200":{"description":"dashboard HTML","content":{"text/html":{}}},"404":{"description":"opaque 404 (gate closed / wrong token)"}}}},"/admin/seed/baseline":{"post":{"tags":["operator"],"operationId":"seedBaseline","summary":"Idempotently install the baseline world","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"responses":{"200":{"description":"{ok:true, seeded:'baseline'}","content":{"application/json":{}}},"404":{"description":"opaque 404"}}}},"/admin/seed":{"post":{"tags":["operator"],"operationId":"seedScenario","summary":"Run a JSON scenario (raw body or dashboard __json form field)","description":"Ops: create, bulk, reply, edit, delete, create-account, create-location. See apps/fibgle/docs/operator-usage.md for the full schema.","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"token":{"type":"string"},"__json":{"type":"string"}}}}}},"responses":{"200":{"description":"ScenarioResult","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorResult"}}}},"400":{"description":"invalid scenario — a runner failure carries the full OperatorResult; a malformed JSON body carries only OperatorError ({ok:false, errors})","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OperatorResult"},{"$ref":"#/components/schemas/OperatorError"}]}}}},"404":{"description":"opaque 404"}}}},"/admin/seed/pack":{"post":{"tags":["operator"],"operationId":"seedPack","summary":"Stand up a multi-business world from a named or inline pack","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["pack"],"properties":{"pack":{"type":"string","enum":["amsterdam-real-businesses","nl-restaurants","us-diners"]}}},{"$ref":"#/components/schemas/SeedPack"}]}}}},"responses":{"200":{"description":"ScenarioResult","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorResult"}}}},"400":{"description":"unknown/invalid pack — an unknown name or runner failure carries the full OperatorResult; a malformed JSON body carries only OperatorError ({ok:false, errors})","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OperatorResult"},{"$ref":"#/components/schemas/OperatorError"}]}}}},"404":{"description":"opaque 404"}}}},"/admin/accounts":{"post":{"tags":["operator"],"operationId":"provisionAccount","summary":"Provision an account + connectable grant (NON-Google; idempotent on accountId)","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionAccountInput"}}}},"responses":{"201":{"description":"{ok:true, account, grant}","content":{"application/json":{}}},"400":{"description":"invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorError"}}}},"404":{"description":"opaque 404"}}}},"/admin/accounts/{accountId}/locations":{"post":{"tags":["operator"],"operationId":"provisionLocation","summary":"Provision a location under an existing account (404 if the account is unknown)","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionLocationInput"}}}},"responses":{"201":{"description":"{ok:true, location}","content":{"application/json":{}}},"400":{"description":"invalid input (e.g. missing title)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorError"}}}},"404":{"description":"unknown account, or opaque 404 when gated","content":{"application/json":{}}}}}},"/admin/reviews/{reviewId}/reply":{"post":{"tags":["operator"],"operationId":"operatorReply","summary":"Operator upsert reply {comment} (404 if no row, 400 if empty)","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"parameters":[{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string"}}}}}},"responses":{"200":{"description":"{ok:true, updateTime}","content":{"application/json":{}}},"400":{"description":"empty comment","content":{"application/json":{}}},"404":{"description":"no such review / opaque 404","content":{"application/json":{}}}}}},"/admin/reviews/{reviewId}/edit":{"post":{"tags":["operator"],"operationId":"operatorEdit","summary":"Operator edit {starRating?, comment?} + bump updateTime","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"parameters":[{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"starRating":{"$ref":"#/components/schemas/StarRating"},"comment":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"{ok:true}","content":{"application/json":{}}},"404":{"description":"no such review / opaque 404","content":{"application/json":{}}}}}},"/admin/reviews/{reviewId}/delete":{"post":{"tags":["operator"],"operationId":"operatorDelete","summary":"Operator delete a review by id (404 if not found)","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"parameters":[{"name":"reviewId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ok:true}","content":{"application/json":{}}},"404":{"description":"no such review / opaque 404","content":{"application/json":{}}}}}},"/sim/tokens/expire":{"post":{"tags":["operator"],"operationId":"expireTokens","summary":"Force-expire access token(s) to fire the 401→refresh→retry path","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string","description":"Omit to expire ALL access tokens."}}}}}},"responses":{"200":{"description":"{ok:true, expired:'one'|'all'}","content":{"application/json":{}}},"404":{"description":"opaque 404"}}}},"/admin/locations":{"post":{"tags":["operator"],"operationId":"provisionLocationFlat","summary":"Provision a location with the parent accountId in the BODY — the console's form-friendly twin of /admin/accounts/{accountId}/locations (same write path).","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProvisionLocationInput"},{"type":"object","required":["account"],"properties":{"account":{"type":"string","description":"Parent accountId."}}}]}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"token":{"type":"string"},"account":{"type":"string"},"title":{"type":"string"},"primaryCategory":{"type":"string"},"primaryCategoryDisplay":{"type":"string"},"locality":{"type":"string"},"regionCode":{"type":"string"},"addressLine":{"type":"string"},"phoneNumber":{"type":"string"},"websiteUri":{"type":"string"}}}}}},"responses":{"201":{"description":"{ok:true, location}","content":{"application/json":{}}},"303":{"description":"console form submission — redirect back to /admin with a flash"},"400":{"description":"invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorError"}}}},"404":{"description":"unknown account, or opaque 404 when gated","content":{"application/json":{}}}}}},"/sim/oauth/mint":{"post":{"tags":["operator"],"operationId":"mintToken","summary":"Issue an access + refresh token pair for a mock user WITHOUT the authorize/consent round-trip (one-click connect-as). NON-Google; the real /token flow is unchanged.","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"string","description":"Mock user (default user-rosie-demo)."},"clientId":{"type":"string"},"scope":{"type":"string"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"token":{"type":"string"},"user":{"type":"string"}}}}}},"responses":{"201":{"description":"{ok:true, access_token, refresh_token, expires_in, scope, token_type, grantId, user}","content":{"application/json":{}}},"303":{"description":"console form submission — redirect back to /admin#tokens"},"404":{"description":"opaque 404"}}}},"/sim/grants/{grantId}/revoke":{"post":{"tags":["operator"],"operationId":"revokeGrant","summary":"Revoke a grant by id — the ergonomic operator twin of /revoke (grant-authoritative cascade to every token under it).","security":[{"seedTokenHeader":[]},{"seedTokenQuery":[]}],"parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ok:true, revoked}","content":{"application/json":{}}},"303":{"description":"console form submission — redirect back to /admin#tokens"},"404":{"description":"unknown grant, or opaque 404 when gated","content":{"application/json":{}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An OAuth access token with the business.manage scope (data APIs)."},"seedTokenHeader":{"type":"apiKey","in":"header","name":"x-fibgle-seed-token","description":"Operator key (FIBGLE_SEED_TOKEN). Surfaces are 404 when the token is UNSET (secure default) and on a wrong token (opaque)."},"seedTokenQuery":{"type":"apiKey","in":"query","name":"token","description":"Same operator key as a ?token= query param (or a hidden form field for the dashboard)."}},"schemas":{"OAuthError":{"type":"object","description":"FLAT OAuth error envelope (oauth surfaces only).","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"RpcStatus":{"type":"object","description":"google.rpc.Status envelope (v1 data APIs).","required":["error"],"properties":{"error":{"type":"object","required":["code","message","status"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"status":{"type":"string","example":"NOT_FOUND"},"details":{"type":"array","items":{"type":"object"}}}}}},"RpcStatusV4":{"type":"object","description":"google.rpc.Status PLUS the legacy error.errors[] array (the v4/mybusiness host).","required":["error"],"properties":{"error":{"type":"object","required":["code","message","status"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"status":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"reason":{"type":"string"},"message":{"type":"string"}}}},"details":{"type":"array","items":{"type":"object"}}}}}},"TokenRequest":{"type":"object","required":["grant_type"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string"},"client_secret":{"type":"string"},"code":{"type":"string","description":"authorization_code grant"},"redirect_uri":{"type":"string","description":"authorization_code grant (must match authorize)"},"code_verifier":{"type":"string","description":"PKCE verifier (required iff a challenge was sent at authorize)"},"refresh_token":{"type":"string","description":"refresh_token grant"}}},"TokenResponse":{"type":"object","required":["access_token","expires_in","scope","token_type"],"properties":{"access_token":{"type":"string","example":"ya29.fibgle-…"},"expires_in":{"type":"integer","example":3599},"scope":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"refresh_token":{"type":"string","description":"Present on the code grant; ABSENT on the refresh grant (no rotation)."}}},"UserInfo":{"type":"object","properties":{"sub":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"name":{"type":"string"},"picture":{"type":"string"}}},"Account":{"type":"object","description":"The field is `accountName` (NOT displayName). `role` is per-(user,account), projected from the grant.","required":["name","accountName","type","role","verificationState","vettedState"],"properties":{"name":{"type":"string","example":"accounts/100000000000000000001"},"accountName":{"type":"string"},"type":{"type":"string","enum":["PERSONAL","LOCATION_GROUP","USER_GROUP","ORGANIZATION"]},"role":{"type":"string","enum":["PRIMARY_OWNER","OWNER","MANAGER","SITE_MANAGER"]},"verificationState":{"type":"string"},"vettedState":{"type":"string"},"permissionLevel":{"type":"string","description":"Output-only; present only when set."},"accountNumber":{"type":"string"},"organizationInfo":{"type":"object"}}},"ListAccountsResponse":{"type":"object","required":["accounts"],"properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/Account"}},"nextPageToken":{"type":"string","description":"ABSENT on the last page."}}},"LocationMetadata":{"type":"object","properties":{"placeId":{"type":"string","description":"Maps place id (ChIJ…-style), a DISTINCT namespace from locationId.","example":"ChIJN1t_tDeuEmsRUsoyG83frY4"},"mapsUri":{"type":"string"},"newReviewUri":{"type":"string"}}},"Location":{"type":"object","description":"Projected to the request's readMask. The FULL resource (readMask=*) is shown here.","properties":{"name":{"type":"string","example":"locations/200000000000000000001"},"title":{"type":"string"},"languageCode":{"type":"string","example":"en"},"storefrontAddress":{"type":"object","properties":{"regionCode":{"type":"string"},"languageCode":{"type":"string"},"postalCode":{"type":"string"},"administrativeArea":{"type":"string"},"locality":{"type":"string"},"addressLines":{"type":"array","items":{"type":"string"}}}},"phoneNumbers":{"type":"object","properties":{"primaryPhone":{"type":"string"}}},"websiteUri":{"type":"string"},"categories":{"type":"object","properties":{"primaryCategory":{"type":"object","properties":{"name":{"type":"string","example":"gcid:restaurant"},"displayName":{"type":"string"}}}}},"metadata":{"$ref":"#/components/schemas/LocationMetadata"}}},"ListLocationsResponse":{"type":"object","required":["locations"],"properties":{"locations":{"type":"array","items":{"$ref":"#/components/schemas/Location"}},"nextPageToken":{"type":"string","description":"ABSENT on the last page."},"totalSize":{"type":"integer","description":"Emitted ONLY when a filter is present."}}},"StarRating":{"type":"string","enum":["STAR_RATING_UNSPECIFIED","ONE","TWO","THREE","FOUR","FIVE"],"description":"The word-string enum on the wire — never an int."},"Reviewer":{"type":"object","required":["isAnonymous"],"description":"displayName/profilePhotoUrl are ABSENT when isAnonymous is true; profilePhotoUrl is also absent for a named reviewer with no photo set.","properties":{"displayName":{"type":"string"},"profilePhotoUrl":{"type":"string"},"isAnonymous":{"type":"boolean"}}},"ReviewReply":{"type":"object","required":["comment","updateTime"],"properties":{"comment":{"type":"string"},"updateTime":{"type":"string","format":"date-time"}}},"Review":{"type":"object","description":"Optional fields are ABSENT, not empty: rating-only reviews omit `comment`; no reply omits `reviewReply`. Timestamps are RFC-3339 with trailing-zero fractions trimmed.","required":["name","reviewId","reviewer","starRating","createTime","updateTime"],"properties":{"name":{"type":"string","example":"accounts/{a}/locations/{l}/reviews/{id}"},"reviewId":{"type":"string","description":"Opaque, URL-safe."},"reviewer":{"$ref":"#/components/schemas/Reviewer"},"starRating":{"$ref":"#/components/schemas/StarRating"},"comment":{"type":"string"},"createTime":{"type":"string","format":"date-time"},"updateTime":{"type":"string","format":"date-time"},"reviewReply":{"$ref":"#/components/schemas/ReviewReply"}}},"ListReviewsResponse":{"type":"object","required":["reviews","totalReviewCount"],"properties":{"reviews":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"averageRating":{"type":"number","description":"1-decimal float on a 1–5 scale; OMITTED when there are no rated reviews. Rides on every page."},"totalReviewCount":{"type":"integer","description":"Lifetime count; rides on every page."},"nextPageToken":{"type":"string","description":"ABSENT on the last page."}}},"OperatorResult":{"type":"object","description":"Fibgle-only scenario/pack result.","required":["ok","applied","created","accounts","locations","errors"],"properties":{"ok":{"type":"boolean"},"applied":{"type":"integer"},"created":{"type":"array","items":{"type":"string"},"description":"Minted reviewIds."},"accounts":{"type":"array","items":{"type":"object","properties":{"ref":{"type":"string"},"accountId":{"type":"string"}}}},"locations":{"type":"array","items":{"type":"object","properties":{"ref":{"type":"string"},"locationId":{"type":"string"},"accountId":{"type":"string"}}}},"errors":{"type":"array","items":{"type":"string"}}}},"OperatorError":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"string"},"errors":{"type":"array","items":{"type":"string"}}}},"ProvisionAccountInput":{"type":"object","required":["accountName"],"properties":{"accountName":{"type":"string"},"type":{"type":"string","enum":["PERSONAL","LOCATION_GROUP","USER_GROUP","ORGANIZATION"]},"verificationState":{"type":"string","enum":["VERIFICATION_STATE_UNSPECIFIED","VERIFIED","UNVERIFIED","VERIFICATION_REQUESTED"]},"accountId":{"type":"string","description":"Supplied-mint idempotency key (minted when omitted)."},"grant":{"type":"object","properties":{"user":{"type":"string"},"role":{"type":"string","enum":["PRIMARY_OWNER","OWNER","MANAGER","SITE_MANAGER"]},"permissionLevel":{"type":"string"}}}}},"ProvisionLocationInput":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"primaryCategory":{"type":"string","example":"gcid:restaurant"},"primaryCategoryDisplay":{"type":"string"},"storefrontAddress":{"type":"object"},"phoneNumber":{"type":"string"},"websiteUri":{"type":"string"},"locationId":{"type":"string","description":"Supplied-mint idempotency key (minted when omitted)."}}},"Scenario":{"type":"object","description":"JSON scenario for /admin/seed. See apps/fibgle/docs/operator-usage.md for the full op union.","properties":{"locationId":{"type":"string","description":"Default review-op target."},"ops":{"type":"array","items":{"type":"object","properties":{"op":{"type":"string","enum":["create","bulk","reply","edit","delete","create-account","create-location"]}}}}}},"SeedPack":{"type":"object","required":["name","profiles"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"profiles":{"type":"array","items":{"type":"object","required":["account","locations"],"properties":{"account":{"$ref":"#/components/schemas/PackAccount"},"locations":{"type":"array","items":{"$ref":"#/components/schemas/PackLocation"}}}}}}},"PackAccount":{"type":"object","required":["accountName"],"properties":{"accountName":{"type":"string"},"type":{"type":"string"},"verificationState":{"type":"string"},"accountId":{"type":"string","description":"Supplied-mint idempotency key (minted when omitted)."},"grant":{"type":"object","properties":{"user":{"type":"string"},"role":{"type":"string"},"permissionLevel":{"type":"string"}}}}},"PackLocation":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"primaryCategory":{"type":"string","example":"gcid:restaurant"},"primaryCategoryDisplay":{"type":"string"},"storefrontAddress":{"type":"object"},"phoneNumber":{"type":"string"},"websiteUri":{"type":"string"},"locationId":{"type":"string","description":"Supplied-mint idempotency key (minted when omitted). Pack reruns replace reviews for supplied location ids."},"reviews":{"$ref":"#/components/schemas/PackReviews"},"reviewFixtures":{"type":"array","items":{"$ref":"#/components/schemas/PackReviewFixture"}}}},"PackReviews":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":500},"targetAverage":{"type":"number","minimum":1,"maximum":5},"locale":{"type":"string"},"anonymousRate":{"type":"number","minimum":0,"maximum":1},"replyRate":{"type":"number","minimum":0,"maximum":1},"spreadDays":{"type":"integer","minimum":0},"baseTime":{"type":"string","format":"date-time"}}},"PackReviewFixture":{"type":"object","properties":{"starRating":{"$ref":"#/components/schemas/StarRating"},"comment":{"type":["string","null"]},"reviewer":{"type":"object","properties":{"displayName":{"type":"string"},"profilePhotoUrl":{"type":"string"},"isAnonymous":{"type":"boolean"}}},"createTime":{"type":"string","format":"date-time"},"reply":{"type":"string","description":"Optional non-empty owner reply text."}}}}}}